1 Commits

Author SHA1 Message Date
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