Container images
What ghcr.io/sandros94/scafo contains and how to run it, plus the scafo-caddy ingress images and their two flavours.
#ghcr.io/sandros94/scafo
#Tags
| Tag | Published by |
|---|---|
:X.Y.Z | A release, tagged with the bare semver; the git tag's v is dropped. |
:latest | The most recent release that is not a prerelease. A prerelease never moves this pointer. |
:edge | Every commit on main that passed the full test suite. |
:sha-<7> | The same commit, by its short SHA. |
:X.Y.Z and :sha-<7> each name one build; :latest and :edge are pointers that move.
#What it bundles
node:24-slimas the base, plusca-certificates, because the slim base ships no root certificate store.- Celld 0.6.2 at
/usr/local/bin/celld, copied fromghcr.io/denoland/celld:0.6.2@sha256:b85983dcf410bcf710bbc30f400d8654123cb2b541ca4d84d7e299009a6ee97a, which is how the Dockerfile pins it: by tag and by multi-architecture digest. - The platform-specific esbuild binary at
/usr/local/bin/esbuild, withENV CELLD_ESBUILD=/usr/local/bin/esbuildso Celld source builds reach it. Nodes need esbuild only to bundle Worker entries;scafo deploy --no-bundleships a pre-bundled entry and skips it. - The container engine's
dockerclient at/usr/local/bin/docker, from Docker's static release 29.8.1, pinned by release and by each architecture's SHA-256. It is the client and no engine: it reaches one only through a socket you mount, and grants the node nothing without one.celld deploybuilds or pulls a container class's image through it on the node that accepts the deploy. - The built application at
/app, withdist/, its productionnode_modules, andpackage.json, which the daemon reads its version from at runtime.
#Runtime settings
ENV SCAFO_STATE_DIR=/var/lib/scafo
USER scafo
EXPOSE 7400
HEALTHCHECK --interval=5s --timeout=3s --start-period=30s --retries=3 CMD ["node", "/app/dist/cli/index.mjs", "health"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["node", "/app/dist/cli/index.mjs"]
CMD ["sail"]- User.
scafo, uid and gid 10001, a non-login user whose home is/var/lib/scafo, created mode0700. Anything bind-mounted there must be owned by 10001. A node given the container engine's socket also needs the socket's group as a supplementary one,group_addin Compose or--group-addfordocker run; see container classes. - State.
SCAFO_STATE_DIR=/var/lib/scafoholds the node identity, the ingress token, the working directory and ports of every runtime process this node supervises and, on a WireGuard node, the tunnel keys. It must be on storage that survives the container being re-created, not only restarted. The image declares noVOLUME, so nothing is kept unless you mount something there. - Port.
EXPOSE 7400only documents the listener; it publishes nothing. The default bind is127.0.0.1:7400, and a wider one is aSCAFO_TRANSPORTdecision. No recipe publishes this port to a host: the ingress is beside the node, in its network namespace or on its container network, and operators reach the API at the platform apex. See the node API. - Healthcheck. It runs
scafo health, which reads the same environment and config file the daemon boots from and asks whatever they say the node bound, so it follows the listener onto a tunnel address or anywhere else. A listener chosen with asail --listenflag is invisible to it: setSCAFO_LISTENornode.listeninstead. - Entrypoint. The entrypoint is the CLI, and
sailis only the default command. Any other argument list runs that subcommand instead.
#Running other commands
Because the entrypoint is the CLI, the image is also how a host without a Scafo binary runs one-off commands. Rendering a WireGuard interface, as the VPS recipe does:
docker run --rm --network host --env-file /etc/scafo/node.env \
-v /var/lib/scafo:/var/lib/scafo \
ghcr.io/sandros94/scafo:X.Y.Z wg init --stdout > /etc/wireguard/wg0.confdocker run --rm --network host --env-file /etc/scafo/node.env \
-v /var/lib/scafo:/var/lib/scafo \
ghcr.io/sandros94/scafo:X.Y.Z wg peers --stdoutThe same shape works for health, fleet nodes, metrics and the rest. Give the container the state directory whenever the command needs the node's identity or keys, and the bucket settings whenever it reads the registry.
Operator ceremonies are not among them. scafo operator keygen --register, scafo remote add --register and scafo trust set want the bucket values and a persistent SCAFO_HOME, and the key file they write has to end up on the operator's own machine; a container that exits takes it with it. Install the CLI where you work and mint the first key there: first deploy.
#Volumes to persist
| Path | Why |
|---|---|
/var/lib/scafo | Node identity, ingress token, tunnel keys, and the working directory and ports of every runtime process. Losing it gives the node a new identity and loses writes this fleet has already acknowledged, because a write is acknowledged once a peer node holds it on its disk, before the bucket has it. |
/var/log/scafo-ingress | Only when SCAFO_INGRESS_ACCESS_LOG is set: the directory the ingress writes its JSON access log into and the node tails. Share it with the ingress container. |
Use a named volume, a retained claim or a real host directory: something that outlives the container being re-created, not only restarted.
Deployments, builds, placements, certificates and observations are in the bucket, not on the host. The most recent writes of a running application may not be yet.
#Stopping
STOPSIGNAL SIGTERM, and the node drains its runtimes on it. It must be the process that receives the signal, and it needs at least 50 seconds before SIGKILL: the shutdown grace and process layout every recipe follows.
#Other build targets
The Dockerfile also carries a probe target: the same Celld and esbuild the image ships, with no application, for suites that bind-mount a worktree's build into a container. It is not part of the default build.
docker build --target probe -t scafo-probe .#ghcr.io/sandros94/scafo-caddy
Caddy with the modules Scafo's Caddy path relies on: the scafo app, which says where each fleet's node is and which token opens it; caddy.storage.scafo, which keeps certificates and private keys in each fleet's bucket through that fleet's node; http.reverse_proxy.upstreams.scafo, which resolves each request's host from the routing every node serves at /v1/ingress/routes; tls.permission.scafo, which decides which names get a certificate on demand; and http.matchers.scafo_plain_http. Upstream Caddy carries none of them. The image runs from environment variables alone, SCAFO_FLEET_ENDPOINT and SCAFO_FLEET_TOKEN for one fleet.
#Flavours
Lean is Caddy with only those modules. -dns is the same build with a curated set of DNS-01 provider plugins compiled in, for a wildcard certificate over the platform domain: the ACME DNS challenge needs a plugin for the zone's provider, and a Caddy plugin can only arrive at compile time.
| Image | Release binaries | |
|---|---|---|
| Lean | :latest, :X.Y.Z, :edge | scafo-caddy-linux-{amd64,arm64} |
| With providers | :latest-dns, :X.Y.Z-dns, :edge-dns | scafo-caddy-dns-linux-{amd64,arm64} |
Images are published for linux/amd64 and linux/arm64. Each release binary ships a .sha256 alongside it and is the same binary that is inside the image of its flavour, lifted out of that build's builder stage. Every release also carries one caddy-version file naming the Caddy both flavours embed.
The lean flavour covers everything but DNS-01: HTTP-01 needs no plugin, and neither does the on-demand TLS that covers customer domains.
#DNS providers in -dns
Bunny, Cloudflare, DigitalOcean, Hetzner, OVH and Route 53: the caddy-dns module for each, pinned to an exact version in the repository's dns-providers.txt.
docker pull ghcr.io/sandros94/scafo-caddy:latest-dns
caddy list-modules | grep -E 'scafo|dns.providers'
# caddy.storage.scafo
# dns.providers.bunny
# dns.providers.cloudflare
# dns.providers.digitalocean
# dns.providers.hetzner
# dns.providers.ovh
# dns.providers.route53
# http.matchers.scafo_plain_http
# http.reverse_proxy.upstreams.scafo
# scafo
# tls.permission.scafoA provider outside that list needs its own build: xcaddy build --with github.com/sandros94/scafo-ingress --with <module>@<version>, or the repository's Dockerfile with its DNS_PROVIDERS build argument.