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

TagPublished by
:X.Y.ZA release, tagged with the bare semver; the git tag's v is dropped.
:latestThe most recent release that is not a prerelease. A prerelease never moves this pointer.
:edgeEvery 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-slim as the base, plus ca-certificates, because the slim base ships no root certificate store.
  • Celld 0.6.2 at /usr/local/bin/celld, copied from ghcr.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, with ENV CELLD_ESBUILD=/usr/local/bin/esbuild so Celld source builds reach it. Nodes need esbuild only to bundle Worker entries; scafo deploy --no-bundle ships a pre-bundled entry and skips it.
  • The container engine's docker client 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 deploy builds or pulls a container class's image through it on the node that accepts the deploy.
  • The built application at /app, with dist/, its production node_modules, and package.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 mode 0700. 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_add in Compose or --group-add for docker run; see container classes.
  • State. SCAFO_STATE_DIR=/var/lib/scafo holds 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 no VOLUME, so nothing is kept unless you mount something there.
  • Port. EXPOSE 7400 only documents the listener; it publishes nothing. The default bind is 127.0.0.1:7400, and a wider one is a SCAFO_TRANSPORT decision. 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 a sail --listen flag is invisible to it: set SCAFO_LISTEN or node.listen instead.
  • Entrypoint. The entrypoint is the CLI, and sail is 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.conf
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 peers --stdout

The 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

PathWhy
/var/lib/scafoNode 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-ingressOnly 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.

Read more in Config > Celld#durability And The State Directory.

#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.

ImageRelease binaries
Lean:latest, :X.Y.Z, :edgescafo-caddy-linux-{amd64,arm64}
With providers:latest-dns, :X.Y.Z-dns, :edge-dnsscafo-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.scafo

A 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.

Read more in Reference > Caddy Modules.