The same node the Ansible role deploys, on one host, without Ansible: the upstream
image (ghcr.io/decdn/decdn-node, amd64 and arm64) under Compose, with the same host
paths, the same start command and the same hardening as the role's systemd unit.
Pick this path for a single machine you already run Docker on. For a fleet, or a host
you want hardened from scratch (firewall, SSH, auto-patching), use the
Ansible project; on Kubernetes, the
Helm chart. docs/requirements.md
compares the paths.
Upstream has not published a release yet, so there is no signed image to pin.
compose.yamlonly takes an image by digest, and a locally built image has no digest until it is pushed somewhere. Until a release exists, see Before a release.
| Host path | In the container | Holds |
|---|---|---|
/etc/decdn/node.toml |
same, read-only | node config, no secrets |
/etc/decdn/keystore.password |
same, read-only | keystore password (decdn, 0600) |
/etc/decdn/decdn.env |
read by Docker, injected as env | DECDN_RPC_URL (root, 0600) |
/var/lib/decdn/ |
same, read-write | node.secret, keystore.json, state, cache |
The layout matches the Ansible role, so the host decdn CLI, backups and restores
(docs/lifecycle.md) work the same way on both.
Networking. The container uses the host network. The daemon's metrics
(127.0.0.1:9090) and admin RPC (127.0.0.1:9191, hard-wired upstream) stay on the
host's loopback, and Docker publishes no ports, so its iptables rules never open
anything past your firewall. The only public port is QUIC udp/4433.
-
A system account and directories:
sudo useradd --system --home-dir /var/lib/decdn --shell /usr/sbin/nologin decdn sudo install -d -m 0700 -o decdn -g decdn /var/lib/decdn sudo install -d -m 0750 -o decdn -g decdn /etc/decdn
-
The
decdnCLI on the host. The image is daemon-only. Install the CLI from the release tarball, checking it against the GPG-signedSHA256SUMSfirst (the maintainer key is indecdn/decdn'sKEYS; its fingerprint is in SECURITY.md). Until a release exists, build it:cargo build --release -p decdn-cli. -
Keys. Create the password file first (
key-genreads it, never creates it), then generate the node key and eth keystore into the data dir:umask 077 openssl rand -base64 32 | sudo -u decdn tee /etc/decdn/keystore.password >/dev/null sudo -u decdn decdn key-gen --output-dir /var/lib/decdn \ --keystore-password-file /etc/decdn/keystore.password
Back them up now (
docs/lifecycle.md). -
Config. Let the CLI write it with the chain's contract addresses already filled in, then point it at
/var/lib/decdnand set the node's region (ISO 3166-1 alpha-2). Add--origin <url>toconfig initif this node has a backing origin.sudo -u decdn decdn config init --chain arbitrum-sepolia --output /etc/decdn/node.toml sudo -u decdn sed -i \ -e 's|^# data_dir = .*|data_dir = "/var/lib/decdn"|' \ -e '0,/^# region = /s|^# region = .*|region = "DE"|' /etc/decdn/node.toml
-
The RPC endpoint, which may embed an API key, goes in a root-only env file:
sudo install -m 0600 -o root -g root compose/decdn.env.example /etc/decdn/decdn.env sudoedit /etc/decdn/decdn.env # DECDN_RPC_URL=https://… -
Check the config against the CLI, with the same environment the daemon gets:
sudo systemd-run --pty --wait --collect -p User=decdn -p EnvironmentFile=/etc/decdn/decdn.env \ /usr/local/bin/decdn config validate --config /etc/decdn/node.toml \ --keystore-password-file /etc/decdn/keystore.password
-
Firewall. Allow inbound udp/4433 on the host firewall and in your cloud provider's security group. Nothing else needs to be open.
-
Start it:
cp compose/.env.example compose/.env $EDITOR compose/.env # DECDN_IMAGE_DIGEST; DECDN_UID/GID = `id -u decdn` / `id -g decdn` sudo docker compose -f compose/compose.yaml up -d curl -s 127.0.0.1:9090/metrics | head # once "node runtime ready" is in the logs decdn node health # admin RPC, from the host
-
Stake and register (on-chain onboarding, ADR 019 Phase 2) with
decdn setup, described inroles/decdn_node/README.md§ On-chain onboarding. The CLI does not readDECDN_RPC_URL, andnode.tomlstill names the public RPCconfig initwrote, so run it through thedecdn_chainhelper indocs/lifecycle.md§ Running on-chain commands, which passes your endpoint as--rpc-url. The node serves paid traffic only after that.
- Logs:
sudo docker compose -f compose/compose.yaml logs -f. To send them to the journal like the systemd unit, switch theloggingdriver (commented incompose.yaml). - Stop:
docker compose stop. It sends SIGTERM, the daemon's graceful drain, and waits up to 300 s. Do not usedecdn node drainhere:restart: unless-stoppedstarts the drained container again. - Upgrade: set the new release's digest as
DECDN_IMAGE_DIGESTin.env, thendocker compose up -d. - Config change: edit
/etc/decdn/node.toml, thendocker compose restart(ordecdn node reloadfor the hot-reloadable sections). - Health: there is no container healthcheck. The image has no HTTP client, and the
metrics listener serves only
/metrics. Probehttp://127.0.0.1:9090/metricsfrom the host, or ship metrics with Grafana Alloy / Prometheus. Dashboards and alert rules are incharts/decdn-node/files/monitoring/.
Build the daemon image from a decdn/decdn checkout (its Dockerfile header shows
how) and push it to a registry on the host's loopback, which gives it a digest:
sudo docker run -d --name registry --restart unless-stopped -p 127.0.0.1:5000:5000 registry:2
sudo docker build -t 127.0.0.1:5000/decdn-node:dev <decdn-checkout> # after staging dist/<arch>/
sudo docker push 127.0.0.1:5000/decdn-node:dev # prints "dev: digest: sha256:… size: …"Then in .env:
DECDN_IMAGE_REPO=127.0.0.1:5000/decdn-node
DECDN_IMAGE_DIGEST=sha256:… # from the push outputThe upstream image is based on Debian bookworm (glibc 2.36). A decdn-node built
natively on a newer distribution will not start in it; build with cross, as the
upstream release does, or on bookworm.
- The container runs as the host's
decdnaccount with a read-only root filesystem, every capability dropped andno-new-privileges. - The two remaining KICS findings are the design, not an oversight: host networking
(loopback-only metrics and admin, no Docker-published ports) and no healthcheck (see
above). The read-only
/etc/decdnmount is excluded from the scan for the reason given in the rootMakefile. - The image is always referenced by digest:
compose.yamlbuildsDECDN_IMAGE_REPO@DECDN_IMAGE_DIGESTitself, so no.envvalue can turn it into a mutable tag. make lint-compose(CI jobcompose) renders this file and fails if any of those properties regress;make test-scriptschecks it rejects broken variants.