mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-09-03 07:24:52 +08:00
feat(demo): run the public demo on Cloudflare Containers
Moves demo.nginxui.com off a VPS and onto a Worker-fronted container
that sleeps when idle.
The Worker never awaits the boot. It checks container state and, when
not ready, starts it in the background and immediately returns a
self-contained loading page at the requested URL — 200, no redirect, so
deep links survive and no redirect loop is possible. API and WebSocket
requests get 503 with Retry-After instead of HTML they cannot parse. The
page is fully inline because nothing under /assets is reachable while
the container that serves it is still starting.
Disk is ephemeral, so a scheduled stop IS the restore; a daily cron
handles the case where nobody idled it out. SIGTERM rather than a kill,
because nginx-ui holds an open SQLite handle.
The image needed rebuilding around four platform constraints, each found
by reproducing them locally rather than reading docs:
1. s6-overlay cannot run. Its preinit chowns /run and its suexec calls
setgid; neither CAP_CHOWN nor CAP_SETGID is granted, and s6 exits
111 before anything starts. Replaced with a plain entrypoint.
2. A `USER` directive prevents scheduling entirely — the instance sits
'inactive' and never gets a location. Runs as root instead.
3. Root is not all-powerful there. With capabilities dropped there is
no CAP_DAC_OVERRIDE, so root obeys file permissions; chowning paths
to `nginx` locked root out of its own filesystem.
4. /run is a fresh tmpfs at runtime, so directories created at build
time are gone and nginx-ui cannot bind its handover socket. The
entrypoint creates them on start.
Cluster peers run as extra nginx-ui processes in the same container on
loopback rather than as separate containers. Node traffic then never
crosses the Worker boundary, never counts as an in-flight request
holding the container awake, and needs no outbound interception.
Also drops NGINX_UI_OFFICIAL_DOCKER: it enabled a Docker socket
self-check that can only fail here, logged OTA cleanup errors on every
boot, and defaulted RestartCmd to `nginx -s stop` — which assumes the s6
supervisor this image no longer has, so restarting nginx from the UI
would have killed the container. The entrypoint supervises nginx
instead.
/__demo/bootlog keeps nginx serving the startup log even when nginx-ui
is down; it is how constraint 4 was found, and without container stdout
it is the only window in. /__demo/recycle forces a fresh container after
a config change that only applies at start.
Image: 509 MB -> 425 MB (105 MB compressed, which is what Cloudflare
distributes), mostly from building with -trimpath -ldflags "-s -w",
which the release workflow already did and this path did not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -29,3 +29,6 @@ log-index/
|
||||
GeoLite2-City.mmdb
|
||||
*.log
|
||||
.claude/settings.local.json
|
||||
|
||||
# Cloudflare Workers local state (miniflare cache, durable object storage)
|
||||
cloudflare/.wrangler/
|
||||
|
||||
30
cloudflare/build-binary.sh
Executable file
30
cloudflare/build-binary.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# Produce the linux/amd64 nginx-ui binary that demo.Dockerfile copies in.
|
||||
#
|
||||
# Cross-compiling from macOS needs a CGO toolchain (the SQLite driver), so this
|
||||
# builds inside a Linux container rather than on the host. The frontend must be
|
||||
# built first: app/dist is embedded into the binary at compile time.
|
||||
set -eu
|
||||
|
||||
repo_root="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
if [ ! -f app/dist/index.html ]; then
|
||||
echo "app/dist is missing; building the frontend first" >&2
|
||||
bun run build
|
||||
fi
|
||||
|
||||
echo "building nginx-ui for linux/amd64 (this takes a few minutes on a cold cache)"
|
||||
# Same flags as the release build in .github/workflows/build.yml, plus -s.
|
||||
# An unstripped binary is ~166 MB, which dominates the image and therefore the
|
||||
# cold start, since Cloudflare distributes the image before an instance runs.
|
||||
docker run --rm --platform linux/amd64 \
|
||||
-v "$repo_root":/src -w /src \
|
||||
-v "$HOME/go/pkg/mod":/go/pkg/mod \
|
||||
-e GOWORK=off -e CGO_ENABLED=1 -e GOOS=linux -e GOARCH=amd64 \
|
||||
golang:1.26-trixie \
|
||||
go build -trimpath -tags=jsoniter \
|
||||
-ldflags "-s -w -X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'" \
|
||||
-o /src/nginx-ui-linux-amd64/nginx-ui main.go
|
||||
|
||||
ls -la nginx-ui-linux-amd64/nginx-ui
|
||||
205
cloudflare/bun.lock
Normal file
205
cloudflare/bun.lock
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "nginx-ui-demo-worker",
|
||||
"dependencies": {
|
||||
"@cloudflare/containers": "^0.3.7",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^5.20260801.1",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.118.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cloudflare/containers": ["@cloudflare/containers@0.3.7", "", {}, "sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260730.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260730.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260730.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260730.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260730.1", "", { "os": "win32", "cpu": "x64" }, "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260801.1", "", {}, "sha512-XCv5xWi47WQOK0LpLa6997Mrpz8Ct+nZmp/M5Xp8Z4BFsarf7nYjkznGOcOoYK5m1GfbMFEEuQ2OIZnbIWoe9A=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.18", "", {}, "sha512-Q5USMGPOLp/dpVE0EA11QGuFyLAccJDKvsrmfqY/ZD540x/3kKlwtvuUxMqzNrOsGE/H4VtXe75EWma0dj/0jA=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"miniflare": ["miniflare@5.20260730.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.28.0", "workerd": "1.20260730.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
"workerd": ["workerd@1.20260730.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260730.1", "@cloudflare/workerd-darwin-arm64": "1.20260730.1", "@cloudflare/workerd-linux-64": "1.20260730.1", "@cloudflare/workerd-linux-arm64": "1.20260730.1", "@cloudflare/workerd-windows-64": "1.20260730.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA=="],
|
||||
|
||||
"wrangler": ["wrangler@4.118.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260730.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260730.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260730.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
}
|
||||
}
|
||||
20
cloudflare/package.json
Normal file
20
cloudflare/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "nginx-ui-demo-worker",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:binary": "sh ./build-binary.sh",
|
||||
"dev": "sh ./build-binary.sh && wrangler dev -c wrangler.dev.jsonc",
|
||||
"deploy": "sh ./build-binary.sh && wrangler deploy",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"types": "wrangler types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/containers": "^0.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^5.20260801.1",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.118.0"
|
||||
}
|
||||
}
|
||||
252
cloudflare/src/index.ts
Normal file
252
cloudflare/src/index.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Container, getContainer } from '@cloudflare/containers'
|
||||
import { loadingPage } from './loading'
|
||||
|
||||
interface Env {
|
||||
NGINX_UI_DEMO: DurableObjectNamespace<NginxUiDemo>
|
||||
/**
|
||||
* Shared secret for the manual recycle endpoint. Unset means the endpoint is
|
||||
* disabled, which is the right default: no secret, no lever.
|
||||
* bunx wrangler secret put DEMO_ADMIN_TOKEN
|
||||
*/
|
||||
DEMO_ADMIN_TOKEN?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The demo runs as a single shared instance rather than one per visitor.
|
||||
*
|
||||
* Everyone must see the same nginx configuration: a visitor who edits a site
|
||||
* and reloads has to land back on the container that holds the edit. getRandom
|
||||
* would scatter them across instances and make the demo look broken.
|
||||
*/
|
||||
// Durable Object state persists across container-application deletes, and a DO
|
||||
// that still references a deleted application never binds a new instance. Bump
|
||||
// this to hand the Worker a clean Durable Object when that happens.
|
||||
const INSTANCE = 'demo-v2'
|
||||
|
||||
/** Worker-owned path, never forwarded to the container. */
|
||||
const STATUS_PATH = '/__demo/status'
|
||||
|
||||
/**
|
||||
* Container-served path that must reach nginx even while the container is
|
||||
* still reported as unhealthy — it is how a failed boot gets diagnosed, so
|
||||
* gating it behind readiness would defeat it.
|
||||
*/
|
||||
const BOOT_LOG_PATH = '/__demo/bootlog'
|
||||
|
||||
/** Worker-owned, secret-gated: stop the container so the next request starts it fresh. */
|
||||
const RECYCLE_PATH = '/__demo/recycle'
|
||||
|
||||
export class NginxUiDemo extends Container<Env> {
|
||||
// nginx inside the container listens here; it proxies to nginx-ui on 9000.
|
||||
defaultPort = 8080
|
||||
|
||||
// Long enough that a visitor reading the docs mid-session does not get
|
||||
// dropped, short enough that an idle demo is not billed all night.
|
||||
sleepAfter = '20m'
|
||||
|
||||
// The SPA's own health endpoint (router/routers.go). The default "ping" path
|
||||
// would 404 and delay readiness.
|
||||
pingEndpoint = 'localhost/healthz'
|
||||
|
||||
// Note: the WebSocket origin allowlist lives in resources/demo/app.ini, not
|
||||
// in envVars here. envVars are applied at container start, so they do not
|
||||
// reach an already-running instance on deploy — and a value baked into the
|
||||
// image ships and rolls out with it. Serving this Worker on a hostname other
|
||||
// than the one in app.ini means adding that origin there too, or every
|
||||
// WebSocket upgrade will be rejected.
|
||||
|
||||
/**
|
||||
* Boot in progress, if any. Held on the instance so concurrent requests
|
||||
* during a cold start share one startup rather than racing several.
|
||||
*/
|
||||
private booting?: Promise<void>
|
||||
|
||||
/**
|
||||
* Report whether the container can serve, kicking off a start if not.
|
||||
*
|
||||
* Deliberately does not await the boot: the caller returns a loading page
|
||||
* immediately instead of holding the request open for several seconds, which
|
||||
* is what produces a white screen.
|
||||
*/
|
||||
/** Current container state, for the status endpoint and for debugging. */
|
||||
async status(): Promise<{ ready: boolean, status: string, exitCode?: number }> {
|
||||
const state = await this.getState()
|
||||
return {
|
||||
ready: state.status === 'healthy',
|
||||
status: state.status,
|
||||
// exitCode is only present on the stopped-with-code variant.
|
||||
...('exitCode' in state ? { exitCode: state.exitCode as number } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
async ready(): Promise<boolean> {
|
||||
const state = await this.getState()
|
||||
if (state.status === 'healthy') {
|
||||
return true
|
||||
}
|
||||
|
||||
console.log(`container not ready yet: status=${state.status}`)
|
||||
|
||||
this.booting ??= this.startAndWaitForPorts()
|
||||
.catch((err: unknown) => {
|
||||
console.error('demo container failed to start', err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.booting = undefined
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the demo to its pristine state.
|
||||
*
|
||||
* Container disk is ephemeral, so stopping IS the restore: the next start
|
||||
* comes up from the image with the seeded database and configs back in
|
||||
* place. SIGTERM rather than a kill, because s6-overlay shuts nginx and
|
||||
* nginx-ui down in order and nginx-ui holds an open SQLite handle.
|
||||
*/
|
||||
/**
|
||||
* Stop the container so the next request starts it fresh.
|
||||
*
|
||||
* Needed after changing envVars: they are applied when the container starts,
|
||||
* so a deploy alone leaves the running instance on the old environment.
|
||||
*/
|
||||
async recycle(): Promise<string> {
|
||||
const state = await this.getState()
|
||||
if (state.status === 'stopped' || state.status === 'stopped_with_code') {
|
||||
return 'already-stopped'
|
||||
}
|
||||
await this.stop()
|
||||
return 'stopped'
|
||||
}
|
||||
|
||||
async restore(): Promise<'stopped' | 'already-stopped'> {
|
||||
const state = await this.getState()
|
||||
if (state.status === 'stopped' || state.status === 'stopped_with_code') {
|
||||
// Most days the idle timeout will already have done this.
|
||||
return 'already-stopped'
|
||||
}
|
||||
|
||||
await this.stop()
|
||||
return 'stopped'
|
||||
}
|
||||
|
||||
override onError(error: unknown): Response {
|
||||
console.error('demo container error', error)
|
||||
return new Response('The demo container failed to start.', {
|
||||
status: 502,
|
||||
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** A navigation request is one where showing a loading page makes sense. */
|
||||
function wantsDocument(request: Request): boolean {
|
||||
if (request.method !== 'GET') {
|
||||
return false
|
||||
}
|
||||
const accept = request.headers.get('accept') ?? ''
|
||||
return accept.includes('text/html')
|
||||
}
|
||||
|
||||
function isWebSocketUpgrade(request: Request): boolean {
|
||||
return (request.headers.get('upgrade') ?? '').toLowerCase() === 'websocket'
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp the public scheme and host onto a request before it reaches the
|
||||
* container.
|
||||
*
|
||||
* The Worker-to-container hop is plain HTTP, so without this nginx derives
|
||||
* `http://<host>` while the browser sent `Origin: https://<host>`. Nginx UI's
|
||||
* WebSocket origin check compares the two and rejects every upgrade — the
|
||||
* terminal, the log stream and the cluster monitor all fail with nothing in the
|
||||
* logs to explain it.
|
||||
*/
|
||||
function withForwardedHeaders(request: Request, url: URL): Request {
|
||||
// A WebSocket upgrade is passed through untouched. Reconstructing the
|
||||
// request drops the upgrade in the Workers runtime, and the handshake then
|
||||
// fails with no diagnostic anywhere. The origin problem those headers would
|
||||
// have solved is handled by NGINX_UI_HTTP_WEBSOCKET_TRUSTED_ORIGINS instead.
|
||||
if (isWebSocketUpgrade(request)) {
|
||||
return request
|
||||
}
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set('X-Forwarded-Proto', url.protocol.replace(':', ''))
|
||||
headers.set('X-Forwarded-Host', url.host)
|
||||
return new Request(request, { headers })
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
const container = getContainer(env.NGINX_UI_DEMO, INSTANCE)
|
||||
|
||||
// Answered by the Worker so the loading page has something to poll that
|
||||
// does not depend on the container being up.
|
||||
if (url.pathname === STATUS_PATH) {
|
||||
const status = await container.status()
|
||||
if (!status.ready) {
|
||||
// Kick off a boot so polling the status page actually makes progress.
|
||||
await container.ready()
|
||||
}
|
||||
return Response.json(status, {
|
||||
headers: { 'cache-control': 'no-store' },
|
||||
})
|
||||
}
|
||||
|
||||
// Force a fresh container, for when a config change only takes effect at
|
||||
// container start (envVars) rather than at deploy.
|
||||
if (url.pathname === RECYCLE_PATH) {
|
||||
const supplied = request.headers.get('x-demo-admin-token')
|
||||
if (!env.DEMO_ADMIN_TOKEN || supplied !== env.DEMO_ADMIN_TOKEN) {
|
||||
return new Response('Not found', { status: 404 })
|
||||
}
|
||||
return Response.json({ outcome: await container.recycle() })
|
||||
}
|
||||
|
||||
if (url.pathname === BOOT_LOG_PATH) {
|
||||
// Deliberately unguarded: nginx answers this one even when nginx-ui is
|
||||
// down, which is precisely when it is worth reading.
|
||||
await container.ready()
|
||||
return container.fetch(withForwardedHeaders(request, url))
|
||||
}
|
||||
|
||||
if (await container.ready()) {
|
||||
// fetch(), not containerFetch(): only fetch() carries WebSocket upgrades,
|
||||
// which the terminal, log stream and cluster monitor all rely on.
|
||||
return container.fetch(withForwardedHeaders(request, url))
|
||||
}
|
||||
|
||||
if (wantsDocument(request) && !isWebSocketUpgrade(request)) {
|
||||
// 200 at the originally requested URL, never a redirect: the visitor's
|
||||
// deep link survives, and there is structurally no redirect loop.
|
||||
return new Response(loadingPage(), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// XHR, assets and WebSocket upgrades get a normal retry signal instead of
|
||||
// an HTML page they cannot parse.
|
||||
return new Response('The demo is starting up.', {
|
||||
status: 503,
|
||||
headers: {
|
||||
'retry-after': '3',
|
||||
'cache-control': 'no-store',
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async scheduled(_controller: ScheduledController, env: Env): Promise<void> {
|
||||
const container = getContainer(env.NGINX_UI_DEMO, INSTANCE)
|
||||
const outcome = await container.restore()
|
||||
console.log(`scheduled demo restore: ${outcome}`)
|
||||
},
|
||||
}
|
||||
106
cloudflare/src/loading.ts
Normal file
106
cloudflare/src/loading.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* The page shown while the demo container is booting.
|
||||
*
|
||||
* Must be completely self-contained. During a cold start nothing under
|
||||
* /assets/* is reachable, because the SPA is served from inside the container
|
||||
* that has not started yet — so a single external stylesheet, font or image
|
||||
* reference would leave the loading page itself broken.
|
||||
*/
|
||||
|
||||
const SPINNER = `<svg class="spinner" viewBox="0 0 50 50" aria-hidden="true">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke-width="4" />
|
||||
</svg>`
|
||||
|
||||
export function loadingPage(): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Starting Nginx UI demo…</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
||||
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #fff; color: #1f2328;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #141414; color: #e6e6e6; }
|
||||
.hint { color: #8b949e !important; }
|
||||
.bar { background: #262626 !important; }
|
||||
}
|
||||
.card { width: min(420px, calc(100vw - 48px)); text-align: center; padding: 24px; }
|
||||
.spinner { width: 44px; height: 44px; animation: rotate 1.6s linear infinite; }
|
||||
.spinner circle {
|
||||
stroke: #1677ff; stroke-linecap: round;
|
||||
animation: dash 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes rotate { 100% { transform: rotate(360deg); } }
|
||||
@keyframes dash {
|
||||
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
||||
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -24; }
|
||||
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
||||
}
|
||||
h1 { font-size: 17px; font-weight: 600; margin: 20px 0 8px; }
|
||||
.hint { font-size: 13px; color: #656d76; margin: 0; }
|
||||
.bar { margin-top: 22px; height: 3px; border-radius: 3px; background: #f0f0f0; overflow: hidden; }
|
||||
.bar span {
|
||||
display: block; height: 100%; width: 35%; border-radius: 3px; background: #1677ff;
|
||||
animation: slide 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(340%); }
|
||||
}
|
||||
.slow { margin-top: 18px; font-size: 12.5px; color: #656d76; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
${SPINNER}
|
||||
<h1>Waking the demo up</h1>
|
||||
<p class="hint">This instance sleeps when nobody is using it. First request takes a few seconds.</p>
|
||||
<div class="bar"><span></span></div>
|
||||
<p class="slow" id="slow">Still starting. This can take up to a minute after a new deploy.</p>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var started = Date.now();
|
||||
var delay = 700;
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.getElementById('slow');
|
||||
if (el) el.style.display = 'block';
|
||||
}, 12000);
|
||||
|
||||
function poll() {
|
||||
fetch('/__demo/status', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : { ready: false }; })
|
||||
.then(function (s) {
|
||||
if (s && s.ready) {
|
||||
// Reload rather than navigate, so the deep link the visitor arrived
|
||||
// on is preserved.
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
schedule();
|
||||
})
|
||||
.catch(schedule);
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
// Back off gently, capped, so a long boot does not hammer the edge.
|
||||
delay = Math.min(delay * 1.3, 4000);
|
||||
setTimeout(poll, delay);
|
||||
}
|
||||
|
||||
setTimeout(poll, delay);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
15
cloudflare/tsconfig.json
Normal file
15
cloudflare/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["es2022"],
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
49
cloudflare/wrangler.dev.jsonc
Normal file
49
cloudflare/wrangler.dev.jsonc
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
// Local-only override: builds the container from this working tree instead
|
||||
// of pulling the published image, so `wrangler dev` exercises unreleased
|
||||
// changes. Not used by `wrangler deploy`.
|
||||
//
|
||||
// demo.Dockerfile expects a prebuilt linux/amd64 binary at
|
||||
// nginx-ui-linux-amd64/nginx-ui in the build context; produce it with:
|
||||
//
|
||||
// docker run --rm --platform linux/amd64 -v "$PWD":/src -w /src \
|
||||
// -e GOWORK=off -e CGO_ENABLED=1 -e GOOS=linux -e GOARCH=amd64 \
|
||||
// golang:1.26-trixie go build -o /src/nginx-ui-linux-amd64/nginx-ui main.go
|
||||
//
|
||||
// bunx wrangler dev -c wrangler.dev.jsonc
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "nginx-ui-demo-dev",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2026-07-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "NginxUiDemo",
|
||||
"image": "../demo.Dockerfile",
|
||||
"image_build_context": "..",
|
||||
"image_vars": {
|
||||
"TARGETOS": "linux",
|
||||
"TARGETARCH": "amd64"
|
||||
},
|
||||
"instance_type": "basic",
|
||||
"max_instances": 1
|
||||
}
|
||||
],
|
||||
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NGINX_UI_DEMO",
|
||||
"class_name": "NginxUiDemo"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["NginxUiDemo"]
|
||||
}
|
||||
]
|
||||
}
|
||||
72
cloudflare/wrangler.jsonc
Normal file
72
cloudflare/wrangler.jsonc
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "nginx-ui-demo",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2026-07-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
// Keep the workers.dev URL alive alongside the custom domain: it is the only
|
||||
// way to reach the demo when the custom domain's DNS is being changed.
|
||||
"workers_dev": true,
|
||||
|
||||
// demo.nginxui.com must already exist as a zone on this account. Wrangler
|
||||
// takes over the DNS record when custom_domain is true.
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "demo.nginxui.com",
|
||||
"custom_domain": true
|
||||
}
|
||||
],
|
||||
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "NginxUiDemo",
|
||||
// Built and pushed to Cloudflare's own registry by `wrangler deploy`,
|
||||
// rather than pulled from Docker Hub. That keeps the deployed image and
|
||||
// this working tree in lockstep, and avoids Docker Hub pull limits.
|
||||
//
|
||||
// demo.Dockerfile expects a prebuilt linux/amd64 binary at
|
||||
// nginx-ui-linux-amd64/nginx-ui; `bun run deploy` produces it first.
|
||||
"image": "../demo.Dockerfile",
|
||||
"image_build_context": "..",
|
||||
"image_vars": {
|
||||
"TARGETOS": "linux",
|
||||
"TARGETARCH": "amd64"
|
||||
},
|
||||
// Measured: the container settles around 550 MiB with three nginx-ui
|
||||
// instances plus nginx, so 1 GiB is the smallest tier that fits.
|
||||
"instance_type": "basic",
|
||||
// Not 1. The platform keeps a provisioned instance of its own, and with a
|
||||
// cap of 1 that instance consumes the whole quota — the Durable Object's
|
||||
// instance then sits 'inactive' forever and the container never starts.
|
||||
// Routing is a singleton by instance ID, so only one is ever claimed.
|
||||
"max_instances": 3
|
||||
}
|
||||
],
|
||||
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NGINX_UI_DEMO",
|
||||
"class_name": "NginxUiDemo"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["NginxUiDemo"]
|
||||
}
|
||||
],
|
||||
|
||||
// Daily restore. The container is ephemeral, so stopping it is the restore.
|
||||
// 19:00 UTC is roughly 03:00 in CST, the quietest window for this audience.
|
||||
"triggers": {
|
||||
"crons": ["0 19 * * *"]
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,36 @@
|
||||
# CGO_ENABLED=1 GOOS=linux CC=x86_64-unknown-linux-gnu-gcc CXX=x86_64-unknown-linux-gnu-g++ GOARCH=amd64 go build -ldflags "-X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'" -o nginx-ui -v main.go
|
||||
# Demo image for the public instance on Cloudflare Containers.
|
||||
#
|
||||
# Deliberately does NOT use s6-overlay, unlike the production Dockerfile.
|
||||
# s6's preinit chowns /run and its suexec calls setgid; Cloudflare Containers
|
||||
# grant neither CAP_CHOWN nor CAP_SETGID, so s6 exits 111 before anything
|
||||
# starts. resources/demo/entrypoint.sh starts the same processes directly.
|
||||
#
|
||||
# Build the binary first (see cloudflare/build-binary.sh):
|
||||
# GOWORK=off CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -trimpath \
|
||||
# -tags=jsoniter -ldflags "-s -w" -o nginx-ui-linux-amd64/nginx-ui main.go
|
||||
ARG NGINX_VERSION=latest
|
||||
FROM nginx:${NGINX_VERSION}
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG S6_OVERLAY_VERSION=3.2.1.0
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
# 8080 rather than 80: this image runs unprivileged (see USER below) and cannot
|
||||
# bind a port under 1024.
|
||||
EXPOSE 8080
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV NGINX_UI_WORKING_DIR=/var/run/
|
||||
ENV NGINX_UI_WORKING_DIR=/var/run/nginx-ui
|
||||
# Deliberately NOT setting NGINX_UI_OFFICIAL_DOCKER. It would enable a Docker
|
||||
# socket self-check that can only fail here (no socket is mounted), run OTA
|
||||
# container cleanup that logs errors on every boot, and default RestartCmd to
|
||||
# `nginx -s stop` — which assumes an s6 supervisor that this image no longer
|
||||
# has. resources/demo/app.ini sets RestartCmd explicitly instead, and
|
||||
# entrypoint.sh does the supervising.
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends wget xz-utils logrotate nginx-module-geoip \
|
||||
&& apt-get install -y --no-install-recommends logrotate \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN case "${TARGETARCH}/${TARGETVARIANT}" in \
|
||||
"amd64/"*) S6_ARCH="x86_64" ;; \
|
||||
"arm64/"*) S6_ARCH="aarch64" ;; \
|
||||
"arm/v7"*) S6_ARCH="arm" ;; \
|
||||
"arm/v6"*) S6_ARCH="arm" ;; \
|
||||
"arm/v5"*) S6_ARCH="arm" ;; \
|
||||
"riscv64/"*) S6_ARCH="riscv64" ;; \
|
||||
*) echo "Unsupported arch: ${TARGETARCH}/${TARGETVARIANT}" && exit 1 ;; \
|
||||
esac && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz && \
|
||||
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \
|
||||
wget -O /tmp/s6-overlay-${S6_ARCH}.tar.xz https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz && \
|
||||
tar -C / -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz && \
|
||||
rm -f /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-${S6_ARCH}.tar.xz
|
||||
|
||||
# register nginx service
|
||||
COPY resources/docker/nginx.run /etc/s6-overlay/s6-rc.d/nginx/run
|
||||
RUN echo 'longrun' > /etc/s6-overlay/s6-rc.d/nginx/type && \
|
||||
touch /etc/s6-overlay/s6-rc.d/user/contents.d/nginx
|
||||
|
||||
RUN mkdir -p /usr/local/etc \
|
||||
&& mkdir /etc/nginx/sites-available \
|
||||
&& mkdir /etc/nginx/sites-enabled \
|
||||
@@ -42,16 +38,6 @@ RUN mkdir -p /usr/local/etc \
|
||||
&& mkdir /etc/nginx/streams-enabled \
|
||||
&& cp -r /etc/nginx /usr/local/etc/nginx
|
||||
|
||||
# init config
|
||||
COPY resources/docker/init-config.up /etc/s6-overlay/s6-rc.d/init-config/up
|
||||
COPY resources/docker/init-config.sh /etc/s6-overlay/s6-rc.d/init-config/init-config.sh
|
||||
|
||||
RUN chmod +x /etc/s6-overlay/s6-rc.d/init-config/init-config.sh && \
|
||||
echo 'oneshot' > /etc/s6-overlay/s6-rc.d/init-config/type && \
|
||||
touch /etc/s6-overlay/s6-rc.d/user/contents.d/init-config && \
|
||||
mkdir -p /etc/s6-overlay/s6-rc.d/nginx/dependencies.d && \
|
||||
touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/init-config
|
||||
|
||||
# copy demo config
|
||||
COPY resources/demo/ojbk.me /etc/nginx/sites-available/ojbk.me
|
||||
COPY ["resources/demo/Prime Sponsor", "/etc/nginx/sites-available/Prime Sponsor"]
|
||||
@@ -61,15 +47,15 @@ RUN ln -s "/etc/nginx/sites-available/Prime Sponsor" \
|
||||
COPY resources/demo/app.ini /etc/nginx-ui/app.ini
|
||||
COPY resources/demo/demo.db /etc/nginx-ui/database.db
|
||||
|
||||
# register nginx-ui service
|
||||
COPY resources/docker/nginx-ui.run /etc/s6-overlay/s6-rc.d/nginx-ui/run
|
||||
RUN echo 'longrun' > /etc/s6-overlay/s6-rc.d/nginx-ui/type && \
|
||||
touch /etc/s6-overlay/s6-rc.d/user/contents.d/nginx-ui
|
||||
|
||||
# copy nginx config
|
||||
COPY resources/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY resources/docker/nginx-ui.conf /etc/nginx/conf.d/nginx-ui.conf
|
||||
# The demo uses its own nginx.conf / nginx-ui.conf rather than the ones under
|
||||
# resources/docker: no `user` directive, temp paths under /tmp, and port 8080,
|
||||
# all so the container can run unprivileged. resources/docker/* stays as-is for
|
||||
# the production image.
|
||||
COPY resources/demo/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY resources/demo/nginx-ui.conf /etc/nginx/conf.d/nginx-ui.conf
|
||||
COPY resources/demo/stub_status_nginx-ui.conf /etc/nginx/conf.d/stub_status_nginx-ui.conf
|
||||
COPY resources/docker/nginx-ui.conf.known-hashes /usr/local/share/nginx-ui/nginx-ui.conf.known-hashes
|
||||
|
||||
# copy nginx-ui executable binary
|
||||
COPY nginx-ui-$TARGETOS-$TARGETARCH$TARGETVARIANT/nginx-ui /usr/local/bin/nginx-ui
|
||||
@@ -82,4 +68,30 @@ RUN rm -f /var/log/nginx/access.log && \
|
||||
rm -f /var/log/nginx/error.log && \
|
||||
touch /var/log/nginx/error.log
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
# extra nginx-ui instances so the cluster view has real peers to talk to,
|
||||
# reachable over loopback instead of a second container
|
||||
COPY resources/demo/setup-cluster-nodes.sh /usr/local/bin/setup-cluster-nodes.sh
|
||||
RUN chmod +x /usr/local/bin/setup-cluster-nodes.sh && \
|
||||
/usr/local/bin/setup-cluster-nodes.sh
|
||||
|
||||
COPY resources/demo/entrypoint.sh /usr/local/bin/demo-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/demo-entrypoint.sh
|
||||
|
||||
# Run unprivileged. Cloudflare Containers grant no privileged capabilities, and
|
||||
# nothing here needs them once the writable paths are owned by the runtime user.
|
||||
# Everything stays owned by root, and there is no USER directive. Both are
|
||||
# forced by how Cloudflare Containers run this image:
|
||||
#
|
||||
# 1. With `USER nginx` the instance never got scheduled at all — it sat
|
||||
# 'inactive' indefinitely, while an otherwise identical root image started
|
||||
# normally.
|
||||
# 2. Running as root does NOT mean unrestricted: every capability is dropped,
|
||||
# so without CAP_DAC_OVERRIDE root obeys ordinary file permissions.
|
||||
# Chowning these paths to `nginx` locked root out of its own filesystem and
|
||||
# broke nginx's error log and nginx-ui's handover socket.
|
||||
#
|
||||
# So: root-owned, root-run, and no capabilities. The isolation comes from the
|
||||
# capability set and the per-container VM, not from the uid.
|
||||
RUN mkdir -p /var/cache/nginx /var/lib/nginx /var/run/nginx-ui /etc/nginx-ui
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/demo-entrypoint.sh"]
|
||||
|
||||
@@ -40,6 +40,12 @@ Secret = 4a82fe17c5aab629007462b303c367d2863b7db28b95c8f869f2f815672d41d8
|
||||
[http]
|
||||
GithubProxy =
|
||||
InsecureSkipVerify = false
|
||||
# The Worker-to-container hop is plain HTTP, so nginx-ui derives http://<host>
|
||||
# while the browser sends Origin: https://<host>, and every WebSocket upgrade
|
||||
# is rejected. Naming the public origin is the supported fix.
|
||||
# Deploying this image under a different hostname means adding it here, or
|
||||
# setting NGINX_UI_HTTP_WEBSOCKET_TRUSTED_ORIGINS before the container starts.
|
||||
WebSocketTrustedOrigins = https://demo.nginxui.com
|
||||
|
||||
[logrotate]
|
||||
Enabled = false
|
||||
@@ -51,20 +57,30 @@ AccessLogPath =
|
||||
ErrorLogPath =
|
||||
LogDirWhiteList = /var/log/nginx
|
||||
ConfigDir =
|
||||
PIDPath =
|
||||
# Must match `pid` in resources/demo/nginx.conf. The demo runs unprivileged and
|
||||
# cannot write the compiled-in default under /var/run, and without this override
|
||||
# nginx.IsRunning() reports false and the whole dashboard shows nginx stopped.
|
||||
PIDPath = /tmp/nginx.pid
|
||||
TestConfigCmd =
|
||||
ReloadCmd =
|
||||
RestartCmd =
|
||||
# entrypoint.sh restarts nginx when it exits, so stopping it is a restart.
|
||||
RestartCmd = nginx -s stop
|
||||
|
||||
[nginx_log]
|
||||
AdvancedIndexingEnabled = true
|
||||
IndexingEnabled = true
|
||||
|
||||
[node]
|
||||
Name =
|
||||
Name = demo
|
||||
Secret = 57D079F2-CA8B-412A-B5C0-FDA291C13391
|
||||
InstanceID = 0e6c6b6a-3f1e-4a9f-8f8a-2f9c2b6d41d0
|
||||
SkipInstallation = false
|
||||
Demo = true
|
||||
|
||||
[site_check]
|
||||
Enabled = true
|
||||
Concurrency = 5
|
||||
IntervalSeconds = 900
|
||||
|
||||
[openai]
|
||||
Provider = openai
|
||||
BaseUrl =
|
||||
|
||||
Binary file not shown.
108
resources/demo/entrypoint.sh
Normal file
108
resources/demo/entrypoint.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/sh
|
||||
# Demo container entrypoint.
|
||||
#
|
||||
# The production image uses s6-overlay, which cannot run here: s6's preinit
|
||||
# chowns /run and its suexec calls setgid, and Cloudflare Containers grant
|
||||
# neither CAP_CHOWN nor CAP_SETGID, so s6 exits 111 before anything starts.
|
||||
#
|
||||
# Note also that "runs as root" does not mean unrestricted. Every capability is
|
||||
# dropped, so without CAP_DAC_OVERRIDE root obeys ordinary file permissions and
|
||||
# nothing in the image may be owned by another user.
|
||||
set -eu
|
||||
|
||||
: "${PEER_COUNT:=2}"
|
||||
BOOT_LOG=/var/log/nginx/demo-boot.log
|
||||
|
||||
log() { echo "[entrypoint] $*" | tee -a "$BOOT_LOG"; }
|
||||
|
||||
mkdir -p /var/log/nginx
|
||||
: > "$BOOT_LOG"
|
||||
|
||||
# Seed /etc/nginx on first boot, mirroring resources/docker/init-config.sh but
|
||||
# without the s6 oneshot wrapper.
|
||||
if [ -z "$(ls -A /etc/nginx 2>/dev/null)" ]; then
|
||||
cp -rp /usr/local/etc/nginx/* /etc/nginx/
|
||||
log "initialized /etc/nginx from the bundled template"
|
||||
fi
|
||||
|
||||
# nginx creates its temp dirs lazily; make them now so a request in the first
|
||||
# second does not race directory creation.
|
||||
mkdir -p /tmp/nginx-client-body /tmp/nginx-proxy /tmp/nginx-fastcgi \
|
||||
/tmp/nginx-uwsgi /tmp/nginx-scgi
|
||||
|
||||
# The working directories must be created at RUNTIME, not in the image.
|
||||
# /run is a fresh tmpfs on Cloudflare Containers, so anything mkdir'd there at
|
||||
# build time is gone by the time this script runs, and nginx-ui fails with
|
||||
# "bind: no such file or directory" on its risefront handover socket.
|
||||
mkdir -p /var/run/nginx-ui
|
||||
n=2
|
||||
while [ "$n" -le $((PEER_COUNT + 1)) ]; do
|
||||
mkdir -p "/var/run/nginx-ui-node${n}"
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
# nginx comes up first and stays up for the container's whole life. Keeping it
|
||||
# alive even when nginx-ui is down is deliberate: the platform health check and
|
||||
# the boot log below stay reachable, so a failure is diagnosable instead of
|
||||
# looking like a container that simply never started.
|
||||
nginx -g "daemon off;" >> "$BOOT_LOG" 2>&1 &
|
||||
NGINX_PID=$!
|
||||
log "started nginx (pid $NGINX_PID)"
|
||||
|
||||
start_ui() {
|
||||
name="$1"
|
||||
workdir="$2"
|
||||
config="$3"
|
||||
NGINX_UI_WORKING_DIR="$workdir" nginx-ui --config "$config" >> "$BOOT_LOG" 2>&1 &
|
||||
log "started $name (pid $!)"
|
||||
}
|
||||
|
||||
# Peer nodes for the cluster view. Each needs its own working directory:
|
||||
# risefront derives its handover socket path from it and would otherwise treat
|
||||
# a peer as a hot-reload child of the primary.
|
||||
n=2
|
||||
while [ "$n" -le $((PEER_COUNT + 1)) ]; do
|
||||
conf="/etc/nginx-ui-node${n}/app.ini"
|
||||
if [ -f "$conf" ]; then
|
||||
start_ui "nginx-ui-node${n}" "/var/run/nginx-ui-node${n}" "$conf"
|
||||
fi
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
start_ui nginx-ui "${NGINX_UI_WORKING_DIR:-/var/run/nginx-ui}" /etc/nginx-ui/app.ini
|
||||
|
||||
shutdown() {
|
||||
log "received termination signal"
|
||||
kill -TERM "$NGINX_PID" 2>/dev/null || true
|
||||
# nginx-ui holds an open SQLite handle; give every child a chance to close.
|
||||
pkill -TERM nginx-ui 2>/dev/null || true
|
||||
wait
|
||||
exit 0
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
# Supervise nginx. The UI's "restart nginx" runs `nginx -s stop` (see
|
||||
# RestartCmd in resources/demo/app.ini), so nginx exiting is a normal event
|
||||
# that must bring it straight back rather than end the container.
|
||||
#
|
||||
# A crashed nginx-ui deliberately does NOT take the container down: it keeps
|
||||
# serving 502 plus the boot log at /__demo/bootlog, and an opaque restart loop
|
||||
# is far harder to debug than a 502.
|
||||
restarts=0
|
||||
while true; do
|
||||
if kill -0 "$NGINX_PID" 2>/dev/null; then
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
restarts=$((restarts + 1))
|
||||
if [ "$restarts" -gt 20 ]; then
|
||||
log "nginx exited $restarts times; giving up so the platform recycles the container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "nginx exited; restarting it (attempt $restarts)"
|
||||
sleep 1
|
||||
nginx -g "daemon off;" >> "$BOOT_LOG" 2>&1 &
|
||||
NGINX_PID=$!
|
||||
done
|
||||
44
resources/demo/nginx-ui.conf
Normal file
44
resources/demo/nginx-ui.conf
Normal file
@@ -0,0 +1,44 @@
|
||||
# Demo-only front proxy. Same as resources/docker/nginx-ui.conf except it
|
||||
# listens on 8080 instead of 80, because the demo container runs unprivileged
|
||||
# and cannot bind a port below 1024. The Cloudflare Container binding targets
|
||||
# 8080 directly, so nothing external depends on the port number.
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $forwarded_host {
|
||||
default $http_x_forwarded_host;
|
||||
'' $http_host;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
client_max_body_size 128M;
|
||||
|
||||
# Boot log, so a container whose nginx-ui failed to start can still be
|
||||
# diagnosed from outside. Serving it needs no working backend, which is the
|
||||
# entire point: this is reachable exactly when nothing else is.
|
||||
location = /__demo/bootlog {
|
||||
default_type text/plain;
|
||||
alias /var/log/nginx/demo-boot.log;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Host $forwarded_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://127.0.0.1:9000/;
|
||||
}
|
||||
}
|
||||
51
resources/demo/nginx.conf
Normal file
51
resources/demo/nginx.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
# Demo-only nginx.conf, shaped by how Cloudflare Containers actually run:
|
||||
# as root, but with every capability dropped.
|
||||
#
|
||||
# - `user root` because without CAP_CHOWN nginx cannot hand its temp
|
||||
# directories to a different worker user, and it refuses to start when
|
||||
# that chown fails
|
||||
# - pid and temp paths under /tmp rather than /var/run
|
||||
# - port 8080 in conf.d/nginx-ui.conf, since CAP_NET_BIND_SERVICE is gone too
|
||||
#
|
||||
# Note that dropping all capabilities means root is NOT all-powerful here: with
|
||||
# no CAP_DAC_OVERRIDE it is subject to ordinary file permissions, so nothing in
|
||||
# the image may be owned by another user.
|
||||
user root;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
stream {
|
||||
include /etc/nginx/streams-enabled/*;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
client_body_temp_path /tmp/nginx-client-body;
|
||||
proxy_temp_path /tmp/nginx-proxy;
|
||||
fastcgi_temp_path /tmp/nginx-fastcgi;
|
||||
uwsgi_temp_path /tmp/nginx-uwsgi;
|
||||
scgi_temp_path /tmp/nginx-scgi;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
58
resources/demo/setup-cluster-nodes.sh
Normal file
58
resources/demo/setup-cluster-nodes.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
# Build the peer nodes for the demo cluster view.
|
||||
#
|
||||
# Instead of running a second container, the demo runs extra nginx-ui processes
|
||||
# inside the same container and points the cluster rows at 127.0.0.1. That keeps
|
||||
# node-to-node traffic on loopback, so it never crosses the Cloudflare Worker
|
||||
# boundary, never counts as an in-flight request (which would stop the container
|
||||
# from ever sleeping), and needs no outbound interception.
|
||||
#
|
||||
# Two things must differ per instance:
|
||||
# 1. the config directory, because the SQLite file is resolved as
|
||||
# dir(configPath)/<DatabaseSettings.Name>.db
|
||||
# 2. NGINX_UI_WORKING_DIR, because risefront derives its handover socket path
|
||||
# from it (see prefix_dialer.go NewPrefixDialer). Sharing it makes the
|
||||
# second process attach to the first as a hot-reload child and give up its
|
||||
# own TCP port.
|
||||
set -eu
|
||||
|
||||
: "${PEER_COUNT:=2}"
|
||||
: "${BASE_CONFIG:=/etc/nginx-ui/app.ini}"
|
||||
: "${BASE_DB:=/etc/nginx-ui/database.db}"
|
||||
|
||||
# Fixed instance IDs so the seeded node rows can reference them and so nothing
|
||||
# rewrites app.ini on first boot.
|
||||
peer_instance_id() {
|
||||
case "$1" in
|
||||
2) echo "1b7f4a52-9c3d-4e18-9a71-6d0f2c845b93" ;;
|
||||
3) echo "2c8e5b63-ad4e-4f29-8b62-7e1a3d956c04" ;;
|
||||
*) echo "00000000-0000-4000-8000-00000000000$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
n=2
|
||||
while [ "$n" -le $((PEER_COUNT + 1)) ]; do
|
||||
# primary node holds 9000, so peer n takes 9000 + (n - 1)
|
||||
port=$((8999 + n))
|
||||
conf_dir="/etc/nginx-ui-node${n}"
|
||||
run_dir="/var/run/nginx-ui-node${n}"
|
||||
|
||||
mkdir -p "$conf_dir" "$run_dir"
|
||||
|
||||
# Peers exist to populate the cluster view. They share the single nginx in
|
||||
# this container, so leave the site prober and the log indexer to the
|
||||
# primary node rather than paying for them three times.
|
||||
sed -e "s|^Port .*|Port = ${port}|" \
|
||||
-e "s|^Name .*|Name = demo-node-${n}|" \
|
||||
-e "s|^InstanceID .*|InstanceID = $(peer_instance_id "$n")|" \
|
||||
-e "s|^IndexingEnabled .*|IndexingEnabled = false|" \
|
||||
"$BASE_CONFIG" > "${conf_dir}/app.ini"
|
||||
sed -i "s|^Enabled = true|Enabled = false|" "${conf_dir}/app.ini"
|
||||
|
||||
cp "$BASE_DB" "${conf_dir}/database.db"
|
||||
|
||||
# resources/demo/entrypoint.sh discovers peers by looking for these config
|
||||
# files, so there is nothing else to register.
|
||||
echo "prepared demo peer node ${n} on port ${port} (${conf_dir})"
|
||||
n=$((n + 1))
|
||||
done
|
||||
Reference in New Issue
Block a user