Self-hosted deployment system for Seed nodes. A single script handles
first-time setup, configuration, container orchestration, backups,
and automatic updates.

Quick Start

curl -fsSL https://deploy.seedhypermedia.com | sh

The bootstrap script installs Docker and Bun (if missing), downloads the
deployment engine, and launches an interactive wizard to configure your
node. After setup you manage everything through the seed-deploy CLI.

Architecture

deploy.sh Minimal bootstrap — installs Docker + Bun, downloads deploy.js | v deploy.ts Main deployment engine (bundled to dist/deploy.js) | - Interactive wizard (first run) | - Headless deploy (subsequent runs / cron) | - Full CLI for node management v docker-compose.yml Container definitions for proxy, web, and daemon

Files

| File | Purpose | | -------------------- | ---------------------------------------- | | `deploy.sh` | One-line bootstrap installer | | `deploy.ts` | Deployment engine source (TypeScript) | | `deploy.test.ts` | Test suite (115 tests, 257 assertions) | | `dist/deploy.js` | Committed production bundle (Bun target) | | `docker-compose.yml` | Docker Compose service definitions |

Modes of Operation

1. Interactive Wizard (first run)

When no config.json exists, the script launches a terminal wizard:


    Public hostname — the https:// URL for the node (required)

    Environment — Production / Staging / Development

    Log level — Debug / Info / Warn / Error

    Gateway mode — whether the node serves all known public content

    Analytics — enable Plausible.io traffic dashboard

    Contact email — optional, for security update notifications

The wizard also detects legacy installations (from website_deployment.sh)
and offers a migration path, pre-filling values from the old config.

2. Headless Deploy (subsequent runs)

When config.json exists, the script runs without prompts:


    Self-updates deploy.js from the upstream repo (cron only)

    Fetches docker-compose.yml and compares SHA-256 with the stored hash

    If nothing changed and all containers are healthy, skips redeployment

    Otherwise: pulls images first (while old containers serve traffic),

then recreates containers from cache — minimizes downtime


    Prunes unused Docker images after successful deploy

3. Reconfiguration

seed-deploy deploy --reconfigure

Re-runs the wizard with current values shown as placeholders. Press Tab
to keep a value, type to change it, or press Enter to clear optional
fields. Changed fields are marked with a pencil icon in the summary.

CLI Reference

seed-deploy [command] [options]
| Command | Description | | ---------------- | ------------------------------------------ | | `deploy` | Deploy or update node (default command) | | `stop` | Stop and remove all containers | | `start` | Start containers without re-deploying | | `restart` | Restart all containers | | `status` | Health, versions, connectivity, disk, cron | | `config` | Print current config (secrets redacted) | | `logs [service]` | Tail logs (daemon, web, or proxy) | | `cron [remove]` | Install or remove auto-update cron jobs | | `backup [path]` | Create portable backup of node data | | `restore <file>` | Restore node data from backup archive | | `uninstall` | Remove all containers, data, and config |
| Option | Description | | ----------------- | ----------------------------------------------- | | `--reconfigure` | Re-run the setup wizard to change configuration | | `-h`, `--help` | Show help message | | `-v`, `--version` | Show script version |

Environment Presets

A single "Environment" choice controls multiple settings:

| Environment | Tag | Network | Use Case | | ----------- | -------- | ------- | ------------------------------ | | Production | `latest` | Mainnet | Stable releases (recommended) | | Staging | `dev` | Mainnet | Dev builds on real network | | Development | `dev` | Testnet | Dev builds on isolated network |

Configuration

Stored at <seed-dir>/config.json. User-facing fields:

| Field | Required | Description | | ------------- | -------- | ---------------------------------------- | | `domain` | Yes | Public hostname including `https://` | | `email` | No | Contact email for security notifications | | `environment` | Yes | `prod`, `staging`, or `dev` | | `gateway` | Yes | Serve all known public content | | `analytics` | Yes | Enable Plausible.io web analytics |

Internal fields (managed by the script): compose_url, compose_sha,
compose_envs, release_channel, testnet, link_secret,
last_script_run.

Docker Services

Three core containers, plus optional metrics:

| Container | Image | Ports | Purpose | | ----------- | ------------------- | ---------------- | ------------------- | | seed-proxy | caddy:2 | 80, 443, 443/udp | Reverse proxy + TLS | | seed-web | seedhypermedia/web | 3000 | Web frontend | | seed-daemon | seedhypermedia/site | 56000, 56000/udp | P2P daemon + API | | prometheus | prom/prometheus | — | Metrics collection | | grafana | grafana/grafana | — | Metrics dashboards |

All containers run as the host user (SEED_UID:SEED_GID) — no root inside
containers. All bind mounts use the :z flag for SELinux compatibility.

Automatic Updates

The cron system installs two jobs:

| Schedule | Task | | ----------- | ---------------------------------------------- | | 02:00 daily | Run deploy.js — pull images, update containers | | Every 4 hrs | Prune unused Docker images older than 1 hour |

Install with seed-deploy cron, remove with seed-deploy cron remove.

Backup & Restore

Backup creates a .tar.gz containing config.json, docker-compose.yml,
and the web/, daemon/, and proxy/ data directories. Containers are
stopped during backup for data consistency and restarted after.

seed-deploy backup # default: <seed-dir>/backups/ seed-deploy backup /tmp/my-backup.tgz # custom path

Restore extracts a backup archive, optionally lets you edit the
configuration via the wizard, restores cron jobs, and runs a full deploy.

seed-deploy restore /path/to/backup.tar.gz

Edge Cases Handled


    glibc < 2.25 — deploy.sh checks glibc version before attempting

Bun install. Prints supported OS versions and exits cleanly.


    SELinux — all Docker bind mounts use :z flag. Without it,

Fedora/CentOS/RHEL silently block container access to host files.


    Legacy installations — detects old website_deployment.sh containers

(docker run-based). Stops and removes them before first
docker compose up to avoid name conflicts.


    Non-root operation — containers run as the host user. Caddy binds

ports 80/443 via CAP_NET_BIND_SERVICE file capability. sudo is
only used when creating directories outside the user's home.


    Disk exhaustion — old Docker images are pruned both inline after

deploys and on a 4-hour cron schedule.


    No-change deploys — skipped entirely when compose SHA matches and

all containers are healthy. Shows a hint for --reconfigure.


    Self-update — in headless mode, the script fetches its own latest

version before deploying. Takes effect on the next run.


    rsync non-fatal — the daemon's monitoring config export uses

rsync -rlt (no owner/group) and wraps failures as warnings to avoid
blocking the daemon start.

Environment Variables

For testing and development:

| Variable | Purpose | | ----------------- | ------------------------------------------- | | `SEED_DIR` | Seed directory (default: deploy.js dirname) | | `SEED_DEPLOY_URL` | Base URL for fetching compose + deploy.js | | `SEED_BRANCH` | Git branch for deploy.sh (default: main) |

Example local testing setup:

# Terminal 1: serve files locally cd ops && python3 -m http.server 9999 # Terminal 2: run deploy against local server SEED_DIR=/tmp/seed-test SEED_DEPLOY_URL=http://localhost:9999 sh ops/deploy.sh

Development

cd ops bun install # install dependencies bun test # run test suite bun run build # bundle to dist/deploy.js

The dist/deploy.js bundle is committed to the repo. A CI workflow
verifies the bundle matches the source on every push to ops/.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime

Attributes
name
Seed Node Deployment
Attributes schemaThe fields this document carries.
Inherited from the parent's children attributes schema
Fetching schema…