Files
nginx-ui/cloudflare/wrangler.dev.jsonc
0xJacky df285c4f8e 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>
2026-08-02 00:34:28 +08:00

50 lines
1.3 KiB
JSON

{
// 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"]
}
]
}