Minimal Docker Compose stack for running nostream in production. The relay uses a pre-built image from GHCR; migrations and default settings ship inside that image.
This guide assumes a Linux host with Docker Engine and the Compose plugin
installed. Container images are published automatically after CI succeeds on pushes to
main. See docs/DEPLOYMENT.md for the CI/CD flow.
| Path | Required | Changes with releases? |
|---|---|---|
.env |
Yes | No — your secrets and tuning |
.nostr/data/ |
Created at runtime | No — Postgres data |
docker-compose.yml |
Yes (via bootstrap) | Yes — re-run bootstrap or PR 2 auto-sync |
postgresql.conf |
Yes (via bootstrap) | Rarely |
.nostr/settings.yaml |
Optional | Your overrides only |
Do not copy onto the host: migrations/, knexfile.js, or a full
settings.yaml from older docs. Migrations run from the image; settings
defaults come from the image and merge with any optional overrides file.
From a git checkout on the server (or after copying the deploy/ folder):
chmod +x deploy/bootstrap.sh
./deploy/bootstrap.sh /opt/nostreamEdit /opt/nostream/.env, load ghcr.io/cameri/nostream:main, then:
cd /opt/nostream
docker compose up -dBootstrap copies release-managed files (docker-compose.yml, postgresql.conf)
from this repository. You only maintain .env and optional settings overrides.
- Docker Engine and the Compose plugin
ghcr.io/cameri/nostream:mainloaded on the host (see Image delivery ifdocker pullfails)
/opt/nostream/
├── docker-compose.yml # from deploy/docker-compose.prod.yml
├── postgresql.conf # from repository root
├── .env # secrets (never commit)
└── .nostr/
├── settings.yaml # optional overrides only
└── data/ # Postgres data (created on first start)
| Service | Image | Notes |
|---|---|---|
| nostream | ghcr.io/cameri/nostream:main | pull_policy: never when the image is pre-loaded |
| nostream-db | postgres:15 | |
| nostream-cache | redis:7.0.5-alpine3.16 | |
| nostream-migrate | ghcr.io/cameri/nostream:main | one-shot knex migrate:latest; same image as the relay |
The relay listens on 127.0.0.1:8008. Expose it with a reverse proxy or
tunnel (for example Cloudflare Tunnel).
The relay waits for nostream-migrate to exit 0 before it starts.
Without .nostr/settings.yaml, the relay uses resources/default-settings.yaml
from the container image. When a release adds new settings keys, they appear
automatically from the image defaults.
To override specific values:
cp deploy/settings.yaml.example /opt/nostream/.nostr/settings.yaml
# edit overrides only — not a full copy of default-settings.yaml
# the relay also writes backups and the audit log into .nostr itself, so the
# directory needs to be writable by uid 1000, not just the settings file
chown 1000:1000 /opt/nostream/.nostr /opt/nostream/.nostr/settings.yaml
chmod 600 /opt/nostream/.nostr/settings.yaml
docker compose up -dOr use the admin API/UI once admin.enabled is configured.
docker compose ps
curl -s -H 'Accept: application/nostr+json' http://127.0.0.1:8008/
curl -s http://127.0.0.1:8008/readyzUse the relay HTTP port (default 8008) for deploy and load-balancer probes:
| Endpoint | Type | Behavior | Typical use |
|---|---|---|---|
/healthz |
Liveness | Always 200 OK if the process is running |
Restart unhealthy containers |
/readyz |
Readiness | 200 when Postgres and Redis respond; 503 otherwise |
HAProxy blue/green cutover |
/readyz is unauthenticated and intended for infrastructure. It reuses the same
Postgres and Redis checks as /admin/health without requiring admin auth.
Each dependency ping uses the default 3s timeout (ADMIN_DEPENDENCY_PING_TIMEOUT_MS).
Set your load balancer check timeout above that (for example HAProxy
timeout check 5s) so slow-but-healthy backends do not flap during probes.
Responses are cached in-process for 1s to absorb polling without hammering the DB pool.
Use readiness before routing traffic to a new instance during deploys. On
SIGTERM the relay sets /readyz to 503 with "status":"draining" while
the HTTP listener remains up, rejects new WebSocket connections, drains
existing clients, then closes (WS_DRAIN_TIMEOUT_MS, default 30s). Set
stop_grace_period above that timeout (reference compose uses 45s) so Docker
does not SIGKILL the container mid-drain.
Some hosts cannot reach GHCR over IPv4:
- nostream image: build or pull elsewhere, then
docker save→ transfer →docker load. Keeppull_policy: neveron nostream and nostream-migrate. - postgres / redis: usually on Docker Hub; use save/load if needed.
When a new image is available:
docker pull ghcr.io/cameri/nostream:main # or: docker load -i nostream-main.tar.gz
docker compose up -dIf migrate does not re-run after a load:
docker compose up -d --force-recreate nostream-migrate nostreamWhen compose or postgresql.conf change in a release, re-run bootstrap against
the new checkout (or copy the updated files). Automated sync is planned separately.
./deploy/bootstrap.sh /opt/nostreamExisting .env and .nostr/settings.yaml are preserved.