Skip to content

feat(images): add caddy-sablier image for VM scale-to-zero - #303

Open
arunesh-j wants to merge 1 commit into
mainfrom
feat/caddy-sablier-image
Open

feat(images): add caddy-sablier image for VM scale-to-zero#303
arunesh-j wants to merge 1 commit into
mainfrom
feat/caddy-sablier-image

Conversation

@arunesh-j

@arunesh-j arunesh-j commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Adds images/caddy-sablier — Caddy with the Sablier scale-to-zero plugin compiled in. Consumed by the ZopDay deployer's new scale-to-zero VM component (zopnight, separate PR), which stops idle services on a VM pool and starts them again on the next request.

Why a custom image

The Sablier wake-on-request plugin is a Caddy module: it has to be compiled into the binary with xcaddy and cannot be loaded into a stock Caddy at runtime. Upstream publishes no prebuilt "Caddy + Sablier" binary, so we build one.

The two alternatives were worse: Caddy's official plugin download service always builds the latest Caddy (so we couldn't pin 2.11.3, see lockstep below) and makes every VM install depend on a third-party service being up; and building on the VM would need a Go toolchain everywhere.

It's a carrier, not a runtime

Nothing runs this container in production. ZopDay VM pools run Caddy as a host systemd service (it binds :80/:443 and reads /etc/caddy), so the component pulls the image and copies the binary out:

CID=$(docker create zopdev/caddy-sablier:v2.11.3)
docker cp "$CID:/usr/bin/caddy" /tmp/caddy && docker rm "$CID"

Docker Hub rather than a GitHub release asset because Docker is already a baseline component on every pool VM and those VMs already pull public images — no new registry auth, no new network path.

Version lockstep (the thing to know when bumping)

CADDY_VERSION must match the version the deployer's baseline caddy component installs. Both variants report the same version string and differ only by the extra dormant module — that is exactly what lets the component's skip-download gate treat them as interchangeable. Bumping one without the other makes every prep re-download the binary forever.

The build asserts both invariants the deployer depends on, so a bad bump fails here instead of on a VM six steps later:

  1. caddy version matches ^v<CADDY_VERSION>
  2. caddy list-modules contains http.handlers.sablier

The plugin is inert unless a sablier directive appears in the Caddyfile, so the binary is a safe drop-in replacement for stock Caddy of the same version.

Follows the existing image conventions

  • images/caddy-sablier/{Dockerfile,README.md}, same layout as db-init / opentsdb
  • Builds with the shared Image Deployment workflow, no workflow changes
  • Publishes on tag caddy-sablier-image-v<N>zopdev/caddy-sablier:v<N> + :latest
  • README mirrors db-init's structure (Components & Versions, Prerequisites, Build, Usage, File Structure, Features, Publish, Contributing/CoC/License)

Tag with the Caddy version rather than an incrementing counter, so the published tag matches what the component requests — it derives the tag from its own CADDY_VERSION.

Verification

Built with the workflow's own invocation (context images/):

$ docker build -f images/caddy-sablier/Dockerfile -t caddy-sablier:prcheck images/
$ docker run --rm caddy-sablier:prcheck caddy version
v2.11.3 h1:/vFbdjcs2DtzcWTIxHybf5R5TspYFFThlZffChyBFHg=
$ docker run --rm caddy-sablier:prcheck caddy list-modules | grep sablier
http.handlers.sablier

Beyond the build, this exact image content was validated end-to-end before opening the PR: the rendered VM Caddyfile (route { sablier … { names …; dynamic } reverse_proxy … }) passes caddy validate against the binary, and a multi-arch build of it ran on a canary VM through a full sleep → wake cycle (idle service stopped, next request served the loading page, then the app).

The Sablier wake-on-request plugin is a Caddy module: it must be compiled
into the binary with xcaddy and cannot be loaded into a stock Caddy at
runtime, and upstream publishes no prebuilt Caddy+Sablier binary. This
builds one, pinned to Caddy 2.11.3 + sablier-caddy-plugin v1.0.2.

The image is a carrier, not a runtime. ZopDay VM pools run Caddy as a host
systemd service, so nothing runs this container: the deployer's
scale-to-zero component pulls it and copies /usr/bin/caddy out. Docker Hub
is used rather than a release asset because Docker is already a baseline
component on every pool VM and those VMs already pull public images, so it
needs no new registry auth or network path.

CADDY_VERSION must stay in lockstep with the deployer's baseline caddy
component: both variants report the same version string and differ only by
the extra dormant module, which is what lets the component's skip-download
gate treat them as interchangeable. The build asserts both invariants the
deployer depends on -- the exact version string and the presence of
http.handlers.sablier -- so a bad bump fails the build rather than a VM
install. The plugin is inert without a sablier directive, making the binary
a drop-in replacement for stock Caddy.

Verified: builds with the workflow's own invocation (context images/),
reports v2.11.3, registers http.handlers.sablier, and the rendered VM
Caddyfile passes caddy validate against it.
@arunesh-j
arunesh-j force-pushed the feat/caddy-sablier-image branch from 7bb6da8 to b8381df Compare August 6, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant