Install
Four ways to get Scafo onto a machine: the npm CLI, the Linux installer, the container image, and compiled binaries.
Scafo runs nodes on Linux only. The CLI half runs anywhere Node.js does.
#CLI
npm install --global scafoThis installs the same binary as everything below; it is enough for app, deploy, deployment, trust, operator, remote, fleet, metrics and traces. Running a node also needs Celld, an ingress and, for a tunnel, wireguard-tools.
#Linux installer
curl -fsSL https://scafo.dev/install.sh | shAs an ordinary user it downloads and verifies the four binaries, puts them in ~/.local/bin (SCAFO_INSTALL_BIN_DIR chooses another directory) and prints how to run the node and its ingress by hand. That is a complete install; nothing in it needs root.
Root buys the host wiring: the two systemd units, the scafo and caddy service accounts, /etc/scafo for the settings and /var/lib/scafo for the state, with the binaries in /usr/local/bin. That is the only reason to run it with sudo:
curl -fsSL https://scafo.dev/install.sh | sudo shRoot that wants the binaries alone sets SCAFO_INSTALL_SKIP_UNITS=1. The installer selects the latest GitHub release unless SCAFO_VERSION names an exact one, passed the same way (SCAFO_VERSION=1.2.3 sh, or sudo SCAFO_VERSION=1.2.3 sh).
#What it downloads
Every download is SHA-256 verified before anything on the host changes. A sidecar .sha256 file provides integrity; set the matching checksum variable to authenticate a latest-release download.
| Binary | Source |
|---|---|
scafo | GitHub releases of sandros94/scafo |
celld | Celld 0.6.2, pinned by checksum in the script |
caddy | scafo-caddy from sandros94/scafo-ingress releases, pinned by checksum |
esbuild | esbuild 0.28.2, pinned by checksum |
The ingress it installs is scafo-caddy and only scafo-caddy: it carries the scafo app, caddy.storage.scafo, the scafo dynamic-upstreams source and tls.permission.scafo, which the generated configuration relies on. Upstream Caddy has none of them.
#Environment variables
| Variable | Effect |
|---|---|
SCAFO_VERSION | Install an exact release instead of the latest. |
SCAFO_INSTALL_BASE | Base URL the scafo artifacts are fetched from. |
SCAFO_SHA256 | Expected checksum of the scafo binary. |
SCAFO_CADDY_BASE | Base URL the scafo-caddy artifacts are fetched from. |
SCAFO_CADDY_SHA256 | Expected checksum of the scafo-caddy binary. |
SCAFO_INSTALL_BIN_DIR | Destination directory for the binaries. |
SCAFO_INSTALL_MIRROR | Mirror for the pinned third-party artifacts. |
SCAFO_INSTALL_SKIP_UNITS=1 | Install binaries only; write no systemd units. |
Without an explicit SCAFO_INSTALL_BIN_DIR, the destination is /usr/local/bin when the script runs as root or has non-interactive sudo, and ~/.local/bin otherwise.
#What it writes as root
Units are written only when the script runs as root on a systemd host and SCAFO_INSTALL_SKIP_UNITS is unset.
| Path | Contents |
|---|---|
/etc/systemd/system/scafo-node.service | The node unit. |
/etc/systemd/system/caddy.service | The ingress unit; receives the ingress token through systemd LoadCredential=. |
/etc/scafo/node.env | Mode 0600. Bucket credentials and every other node setting, commented. Preserved across reinstalls. |
/etc/scafo/caddy.json | The ingress configuration: the node declared once, as the one fleet of Caddy's scafo app, with the ingress token read from the unit's credential directory; certificate storage, upstreams and the on-demand permission all read that fleet. Never rewritten if it already exists. |
/var/lib/scafo | Mode 0700, owned by the scafo user. Node identity, ingress token, WireGuard material, install manifest, and the working directory and ports of every runtime process this node supervises. |
/var/log/scafo-ingress/ | Where Caddy writes its access log when asked to. |
/var/lib/caddy | The ingress unit's StateDirectory, holding the ingress's ACME account. Certificates are not here; they are in the bucket. |
<bin dir>/scafo-uninstall | The release's own uninstall.sh, verified and installed beside the binaries. |
/var/lib/scafo/install.manifest | What this run installed, read by scafo-uninstall. |
Two service accounts are created when missing: scafo and caddy.
Important
The installer does not start a node. Fill in SCAFO_S3_ENDPOINT, SCAFO_S3_BUCKET, SCAFO_S3_ACCESS_KEY, SCAFO_S3_SECRET_KEY and SCAFO_PLATFORM_DOMAINS in /etc/scafo/node.env first, then systemctl enable --now scafo-node.service.
Note
On the first boot the ingress unit fails and retries until the node mints the ingress token. Restart=on-failure with StartLimitIntervalSec=0 is what carries it; 243/CREDENTIALS in the first moments of the journal is expected.
#Refusals
The installer stops rather than installing on a host it cannot support:
- Not Linux.
- An architecture other than
x86_64/amd64oraarch64/arm64. - A musl C library (Alpine and similar) — every installed binary is linked against glibc. Use the container image there.
WireGuard is never installed: a tunnel needs a kernel module and NET_ADMIN. Install wireguard-tools with the host package manager.
#Uninstall
scafo-uninstall [--purge] [--dry-run] [--bin-dir <dir>]The installer puts scafo-uninstall beside the binaries: the uninstall.sh of the very release it installed, verified like everything else it downloaded. Should it be gone, the current one takes the same flags straight off the wire:
curl -fsSL https://scafo.dev/uninstall.sh | sudo sh -s -- [--purge] [--dry-run] [--bin-dir <dir>]Either reads the manifest the install wrote and falls back to the default paths without one.
| Flag | Effect |
|---|---|
| (none) | Removes units, executables and /etc/scafo; preserves /var/lib/scafo, ingress state, logs and created accounts. |
--purge | Also removes the state directory: this node's keypair, and the runtime working directories, which may still hold writes the fleet has acknowledged and the bucket has not got yet. |
--dry-run, -n | Prints every removal and performs none. |
--bin-dir <dir> | Where the binaries were installed, when the manifest cannot say. |
Neither mode changes the bucket or its registry. A node that stops is counted out of placement on its own.
#Container image
ghcr.io/sandros94/scafo:<version>| Tag | Meaning |
|---|---|
:<version> | A release. |
:edge | Every commit on main. |
The image bundles Scafo, Celld 0.6.2, esbuild and the container engine's docker client on a node:24-slim base with ca-certificates.
| Property | Value |
|---|---|
| User | scafo, uid/gid 10001 |
| Home and state dir | /var/lib/scafo, mode 0700; SCAFO_STATE_DIR points at it |
| esbuild | /usr/local/bin/esbuild, with CELLD_ESBUILD set to it |
| Celld | /usr/local/bin/celld |
| Container client | /usr/local/bin/docker, the client alone and no engine |
| Exposed port | 7400 |
| Entrypoint | node /app/dist/cli/index.mjs, default command sail |
| Healthcheck | scafo health, every 5s, 3s timeout, 30s start period, 3 retries |
| Stop signal | SIGTERM |
Warning
Keep the direct Node entrypoint as PID 1 in a private PID namespace. Do not use host PID mode or a persistent wrapper. Configure at least 50 seconds of shutdown grace, or CELLD_SHUTDOWN_TOTAL_MS plus at least 10 seconds when that is raised.
The matching ingress image is ghcr.io/sandros94/scafo-caddy:<version|edge>, in a lean flavour and a -dns flavour carrying DNS-01 provider plugins.
#Binaries
pnpm compileProduces build/scafo-linux-amd64 and build/scafo-linux-arm64, each with a .sha256 file. pnpm compile linux-amd64 builds one target. Celld, an ingress and WireGuard tools remain separate prerequisites.
#Bootstrap an operator key
Nothing can be deployed until the fleet trusts a key. With the bucket settings exported into your shell:
scafo operator keygen --registerThe key lands in ./scafo-operator.json, which is where scafo deploy looks for it. --register writes the public half straight into the registry, with no daemon involved. This bucket-side path admits admin tier only: an operator-tier key needs an application scope, and only the node API can grant one.