
# 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

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

```sh
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`:

```sh
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.

| 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`/`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

```sh
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:

```sh
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.

::read-more{to="/deploy/image"}
Image details and container requirements.
::

## Binaries

```sh
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:

```sh
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{to="/docs/operators"}
Operator keys, roles, application scopes, remotes and rotation.
::
