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 | shThe 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 daemonFiles
| 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 --reconfigureRe-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 pathRestore 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.gzEdge 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.shDevelopment
cd ops
bun install # install dependencies
bun test # run test suite
bun run build # bundle to dist/deploy.jsThe dist/deploy.js bundle is committed to the repo. A CI workflow verifies the bundle matches the source on every push to ops/.