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 scafo

This 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 | sh

As 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 sh

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

BinarySource
scafoGitHub releases of sandros94/scafo
celldCelld 0.6.2, pinned by checksum in the script
caddyscafo-caddy from sandros94/scafo-ingress releases, pinned by checksum
esbuildesbuild 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

VariableEffect
SCAFO_VERSIONInstall an exact release instead of the latest.
SCAFO_INSTALL_BASEBase URL the scafo artifacts are fetched from.
SCAFO_SHA256Expected checksum of the scafo binary.
SCAFO_CADDY_BASEBase URL the scafo-caddy artifacts are fetched from.
SCAFO_CADDY_SHA256Expected checksum of the scafo-caddy binary.
SCAFO_INSTALL_BIN_DIRDestination directory for the binaries.
SCAFO_INSTALL_MIRRORMirror for the pinned third-party artifacts.
SCAFO_INSTALL_SKIP_UNITS=1Install 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.

PathContents
/etc/systemd/system/scafo-node.serviceThe node unit.
/etc/systemd/system/caddy.serviceThe ingress unit; receives the ingress token through systemd LoadCredential=.
/etc/scafo/node.envMode 0600. Bucket credentials and every other node setting, commented. Preserved across reinstalls.
/etc/scafo/caddy.jsonThe 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/scafoMode 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/caddyThe ingress unit's StateDirectory, holding the ingress's ACME account. Certificates are not here; they are in the bucket.
<bin dir>/scafo-uninstallThe release's own uninstall.sh, verified and installed beside the binaries.
/var/lib/scafo/install.manifestWhat 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/amd64 or aarch64/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.

FlagEffect
(none)Removes units, executables and /etc/scafo; preserves /var/lib/scafo, ingress state, logs and created accounts.
--purgeAlso 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, -nPrints 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>
TagMeaning
:<version>A release.
:edgeEvery 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.

PropertyValue
Userscafo, 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 port7400
Entrypointnode /app/dist/cli/index.mjs, default command sail
Healthcheckscafo health, every 5s, 3s timeout, 30s start period, 3 retries
Stop signalSIGTERM

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.

Read more in Deploy > Image.

#Binaries

pnpm compile

Produces 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 --register

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

Read more in Docs > Operators.