From 4ef97559ba0fa52a66944b671971cf4628928634 Mon Sep 17 00:00:00 2001 From: ABHAY PANDEY Date: Fri, 28 Aug 2026 12:21:38 +0530 Subject: [PATCH 1/2] ci: add production Docker Compose stack for relay servers --- .changeset/deploy-production-compose.md | 8 +++ deploy/Dockerfile.migrate | 8 +++ deploy/README.md | 67 ++++++++++++++++++ deploy/docker-compose.prod.yml | 91 +++++++++++++++++++++++++ deploy/env.example | 23 +++++++ deploy/settings.yaml.example | 26 +++++++ 6 files changed, 223 insertions(+) create mode 100644 .changeset/deploy-production-compose.md create mode 100644 deploy/Dockerfile.migrate create mode 100644 deploy/README.md create mode 100644 deploy/docker-compose.prod.yml create mode 100644 deploy/env.example create mode 100644 deploy/settings.yaml.example diff --git a/.changeset/deploy-production-compose.md b/.changeset/deploy-production-compose.md new file mode 100644 index 00000000..c1874cfc --- /dev/null +++ b/.changeset/deploy-production-compose.md @@ -0,0 +1,8 @@ +--- +"nostream": patch +--- + +deploy: add production Docker Compose stack for relay servers + +Adds a minimal prod compose file, migrate image Dockerfile, and server layout docs +for deployments that pull `ghcr.io/cameri/nostream:main` instead of building on the host. diff --git a/deploy/Dockerfile.migrate b/deploy/Dockerfile.migrate new file mode 100644 index 00000000..0680b458 --- /dev/null +++ b/deploy/Dockerfile.migrate @@ -0,0 +1,8 @@ +FROM node:24-alpine + +WORKDIR /code + +# Pre-install migration deps on a machine with registry access, then load on the server. +RUN npm install --no-save knex@2.4.0 pg@8.8.0 + +ENTRYPOINT ["npx", "knex", "migrate:latest"] diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..b3d39480 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,67 @@ +# Production deploy (relay.tnsor.network) + +Minimal Docker Compose stack for the Hetzner relay server. Uses a pre-built +GHCR image instead of building on the server. + +## Layout on server + +``` +/opt/nostream/ +├── docker-compose.yml # copy from deploy/docker-compose.prod.yml +├── .env # secrets (never commit) +├── .nostr/ +│ ├── settings.yaml # copy from deploy/settings.yaml.example +│ └── data/ # Postgres data (created on first start) +├── migrations/ # from repo (Part 2) +├── knexfile.js # from repo (Part 2) +└── postgresql.conf # from repo (Part 2) +``` + +## Services + +| Service | Image | Notes | +|-------------------|--------------------------------|--------------------------------| +| nostream | ghcr.io/cameri/nostream:main | `pull_policy: never` on IPv6-only host | +| nostream-db | postgres:15 | | +| nostream-cache | redis:7.0.5-alpine3.16 | | +| nostream-migrate | nostream-migrate:local | pre-built on a machine with npm access | + +Relay binds to `127.0.0.1:8008` for Cloudflare tunnel (Part 5). + +## IPv6-only server notes + +- GHCR pull often fails (IPv4-only registry). Load the image manually: + `docker save` on a machine with access → `scp` → `docker load` on server. +- Set `pull_policy: never` on the nostream service so Compose uses the loaded image. +- If `postgres`, `redis`, or `node` pulls fail, use the same save/load workaround. +- `nostream-migrate` cannot run `npm install` on the server. Build and load the + migrate image from a machine with registry access: + + ```bash + docker build --platform linux/amd64 -f deploy/Dockerfile.migrate -t nostream-migrate:local . + docker save nostream-migrate:local | gzip > /tmp/nostream-migrate.tar.gz + scp -6 /tmp/nostream-migrate.tar.gz ferryx@'[2a01:4f9:c015:13f4::1]':/opt/nostream/ + # on server: + gunzip -c nostream-migrate.tar.gz | docker load + ``` + +## Quick start (server) + +Assumes Part 2 is complete (Docker, `/opt/nostream`, `.env`, nostream image loaded). + +```bash +cd /opt/nostream +mkdir -p .nostr/data .nostr/db-logs +chmod 755 .nostr +chown 1000:1000 .nostr/settings.yaml +chmod 600 .env .nostr/settings.yaml + +docker pull postgres:15 +docker pull redis:7.0.5-alpine3.16 +docker pull node:24-alpine + +docker compose up -d +docker compose logs -f nostream-migrate +docker compose logs -f nostream +curl -s http://127.0.0.1:8008/ | head +``` diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 00000000..0f70c56b --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -0,0 +1,91 @@ +services: + nostream: + image: ghcr.io/cameri/nostream:main + pull_policy: never + container_name: nostream + env_file: .env + environment: + RELAY_PORT: 8008 + NOSTR_CONFIG_DIR: /home/node/.nostr + DB_HOST: nostream-db + DB_PORT: 5432 + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + DB_MIN_POOL_SIZE: ${DB_MIN_POOL_SIZE:-16} + DB_MAX_POOL_SIZE: ${DB_MAX_POOL_SIZE:-64} + DB_ACQUIRE_CONNECTION_TIMEOUT: ${DB_ACQUIRE_CONNECTION_TIMEOUT:-60000} + REDIS_HOST: nostream-cache + REDIS_PORT: 6379 + REDIS_USER: default + REDIS_PASSWORD: ${REDIS_PASSWORD} + READ_REPLICA_ENABLED: 'false' + WORKER_COUNT: ${WORKER_COUNT:-2} + user: node:node + volumes: + - ${PWD}/.nostr:/home/node/.nostr + ports: + - 127.0.0.1:8008:8008 + depends_on: + nostream-cache: + condition: service_healthy + nostream-db: + condition: service_healthy + nostream-migrate: + condition: service_completed_successfully + restart: on-failure + + nostream-db: + image: postgres:15 + container_name: nostream-db + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - ${PWD}/.nostr/data:/var/lib/postgresql/data + - ${PWD}/.nostr/db-logs:/var/log/postgresql + - ${PWD}/postgresql.conf:/postgresql.conf + command: postgres -c 'config_file=/postgresql.conf' + restart: always + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${DB_USER}'] + interval: 5s + timeout: 5s + retries: 5 + start_period: 360s + + nostream-cache: + image: redis:7.0.5-alpine3.16 + container_name: nostream-cache + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + - cache:/data + command: sh -c 'redis-server --loglevel warning --requirepass "$$REDIS_PASSWORD"' + restart: always + healthcheck: + test: ['CMD-SHELL', 'redis-cli -a "$$REDIS_PASSWORD" ping | grep PONG'] + interval: 2s + timeout: 5s + retries: 10 + + nostream-migrate: + image: nostream-migrate:local + pull_policy: never + container_name: nostream-migrate + environment: + DB_HOST: nostream-db + DB_PORT: 5432 + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + volumes: + - ./migrations:/code/migrations + - ./knexfile.js:/code/knexfile.js + depends_on: + nostream-db: + condition: service_healthy + +volumes: + cache: diff --git a/deploy/env.example b/deploy/env.example new file mode 100644 index 00000000..8bdcba1b --- /dev/null +++ b/deploy/env.example @@ -0,0 +1,23 @@ +# Copy to /opt/nostream/.env on the server and replace placeholders. +# chmod 600 .env + +SECRET=change_me_openssl_rand_hex_128 + +DB_HOST=nostream-db +DB_PORT=5432 +DB_USER=nostr_ts_relay +DB_PASSWORD=change_me_openssl_rand_hex_32 +DB_NAME=nostr_ts_relay + +REDIS_HOST=nostream-cache +REDIS_PORT=6379 +REDIS_USER=default +REDIS_PASSWORD=change_me_openssl_rand_hex_32 + +RELAY_PORT=8008 +NOSTR_CONFIG_DIR=/home/node/.nostr + +DB_MIN_POOL_SIZE=16 +DB_MAX_POOL_SIZE=64 +DB_ACQUIRE_CONNECTION_TIMEOUT=60000 +WORKER_COUNT=2 diff --git a/deploy/settings.yaml.example b/deploy/settings.yaml.example new file mode 100644 index 00000000..331c6404 --- /dev/null +++ b/deploy/settings.yaml.example @@ -0,0 +1,26 @@ +# Copy to /opt/nostream/.nostr/settings.yaml on the server. +# Values here override resources/default-settings.yaml from the image. + +info: + relay_url: wss://relay.tnsor.network + name: relay.tnsor.network + description: A Nostr relay powered by nostream. + pubkey: "" + contact: mailto:operator@tnsor.network + terms_of_service: https://relay.tnsor.network/terms + privacy_policy: https://relay.tnsor.network/privacy + +payments: + enabled: false + +nip45: + enabled: true + +nip66: + enabled: false + +workers: + count: 2 + +admin: + enabled: false From 0ab5922a5185cf9c08bef52ab243b107371fc2d8 Mon Sep 17 00:00:00 2001 From: ABHAY PANDEY Date: Fri, 28 Aug 2026 12:52:59 +0530 Subject: [PATCH 2/2] docs: rewrite deploy README --- deploy/README.md | 138 ++++++++++++++++++++++++++--------- deploy/env.example | 2 +- deploy/settings.yaml.example | 2 +- 3 files changed, 104 insertions(+), 38 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index b3d39480..34d278c0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,9 +1,32 @@ -# Production deploy (relay.tnsor.network) +# Production deployment -Minimal Docker Compose stack for the Hetzner relay server. Uses a pre-built -GHCR image instead of building on the server. +Minimal Docker Compose stack for running nostream in production. The relay +container pulls a pre-built image from GHCR instead of building on the server. -## Layout on server +This guide assumes a Linux host with Docker Engine and the Compose plugin +installed. For image publishing on merge to `main`, see +`.github/workflows/publish-container-image.yml`. + +## Prerequisites + +Before deploying the compose stack: + +1. Install [Docker Engine](https://docs.docker.com/engine/install/) and the + Compose plugin on the host. +2. Create a deploy directory (for example `/opt/nostream`). +3. Copy `deploy/docker-compose.prod.yml` to `docker-compose.yml` in that directory. +4. Copy `deploy/settings.yaml.example` to `.nostr/settings.yaml` and edit for + your relay. +5. Create `.env` from `deploy/env.example` with production secrets. +6. Copy from the repository root into the deploy directory: + - `migrations/` + - `knexfile.js` + - `postgresql.conf` +7. Load `ghcr.io/cameri/nostream:main` on the host (see + [Image delivery on restricted networks](#image-delivery-on-restricted-networks) + if `docker pull` fails). + +## Server layout ``` /opt/nostream/ @@ -12,45 +35,28 @@ GHCR image instead of building on the server. ├── .nostr/ │ ├── settings.yaml # copy from deploy/settings.yaml.example │ └── data/ # Postgres data (created on first start) -├── migrations/ # from repo (Part 2) -├── knexfile.js # from repo (Part 2) -└── postgresql.conf # from repo (Part 2) +├── migrations/ # from repository root +├── knexfile.js # from repository root +└── postgresql.conf # from repository root ``` ## Services -| Service | Image | Notes | -|-------------------|--------------------------------|--------------------------------| -| nostream | ghcr.io/cameri/nostream:main | `pull_policy: never` on IPv6-only host | -| nostream-db | postgres:15 | | -| nostream-cache | redis:7.0.5-alpine3.16 | | -| nostream-migrate | nostream-migrate:local | pre-built on a machine with npm access | - -Relay binds to `127.0.0.1:8008` for Cloudflare tunnel (Part 5). +| Service | Image | Notes | +|-------------------|--------------------------------|---------------------------------------------| +| nostream | ghcr.io/cameri/nostream:main | `pull_policy: never` when image is pre-loaded | +| nostream-db | postgres:15 | | +| nostream-cache | redis:7.0.5-alpine3.16 | | +| nostream-migrate | nostream-migrate:local | pre-built; see migrate image build below | -## IPv6-only server notes +The relay listens on `127.0.0.1:8008` by default. Expose it publicly with a +reverse proxy or tunnel (for example Cloudflare Tunnel) in front of that address. -- GHCR pull often fails (IPv4-only registry). Load the image manually: - `docker save` on a machine with access → `scp` → `docker load` on server. -- Set `pull_policy: never` on the nostream service so Compose uses the loaded image. -- If `postgres`, `redis`, or `node` pulls fail, use the same save/load workaround. -- `nostream-migrate` cannot run `npm install` on the server. Build and load the - migrate image from a machine with registry access: - - ```bash - docker build --platform linux/amd64 -f deploy/Dockerfile.migrate -t nostream-migrate:local . - docker save nostream-migrate:local | gzip > /tmp/nostream-migrate.tar.gz - scp -6 /tmp/nostream-migrate.tar.gz ferryx@'[2a01:4f9:c015:13f4::1]':/opt/nostream/ - # on server: - gunzip -c nostream-migrate.tar.gz | docker load - ``` - -## Quick start (server) - -Assumes Part 2 is complete (Docker, `/opt/nostream`, `.env`, nostream image loaded). +## Deploy ```bash cd /opt/nostream + mkdir -p .nostr/data .nostr/db-logs chmod 755 .nostr chown 1000:1000 .nostr/settings.yaml @@ -58,10 +64,70 @@ chmod 600 .env .nostr/settings.yaml docker pull postgres:15 docker pull redis:7.0.5-alpine3.16 -docker pull node:24-alpine docker compose up -d docker compose logs -f nostream-migrate docker compose logs -f nostream -curl -s http://127.0.0.1:8008/ | head ``` + +## Verify + +```bash +docker compose ps +curl -s http://127.0.0.1:8008/ +curl -s -H 'Accept: application/nostr+json' http://127.0.0.1:8008/ +``` + +The second command should return NIP-11 relay metadata JSON. + +## Migrate image + +The migrate service expects a local image tagged `nostream-migrate:local`. Build +it on a machine with registry access (use `linux/amd64` when building on Apple +Silicon): + +```bash +docker build --platform linux/amd64 -f deploy/Dockerfile.migrate -t nostream-migrate:local . +docker save nostream-migrate:local | gzip > nostream-migrate.tar.gz +``` + +Transfer and load on the server: + +```bash +gunzip -c nostream-migrate.tar.gz | docker load +``` + +## Image delivery on restricted networks + +Some hosts cannot reach GHCR or npm over IPv4. Workarounds: + +- **nostream image:** build or pull elsewhere, then `docker save` → transfer → + `docker load` on the server. Keep `pull_policy: never` on the nostream service. +- **postgres / redis:** usually available from Docker Hub; if not, use the same + save/load approach. +- **migrations:** use the pre-built migrate image above instead of running + `npm install` on the server. + +## Settings file permissions + +The nostream container runs as the `node` user (uid 1000). Ensure +`.nostr/settings.yaml` is owned by uid 1000 and readable by that user: + +```bash +chown 1000:1000 .nostr/settings.yaml +chmod 600 .nostr/settings.yaml +``` + +Without this, the relay falls back to default settings from the image. + +## Updating + +When a new image is available: + +```bash +docker load -i nostream-main.tar.gz # if not pulling from GHCR +docker compose up -d +``` + +Migrations re-run automatically via the `nostream-migrate` service on each +`docker compose up`. diff --git a/deploy/env.example b/deploy/env.example index 8bdcba1b..f0bc3826 100644 --- a/deploy/env.example +++ b/deploy/env.example @@ -1,4 +1,4 @@ -# Copy to /opt/nostream/.env on the server and replace placeholders. +# Copy to .env on the server and replace placeholders. # chmod 600 .env SECRET=change_me_openssl_rand_hex_128 diff --git a/deploy/settings.yaml.example b/deploy/settings.yaml.example index 331c6404..54fd61e1 100644 --- a/deploy/settings.yaml.example +++ b/deploy/settings.yaml.example @@ -1,4 +1,4 @@ -# Copy to /opt/nostream/.nostr/settings.yaml on the server. +# Copy to .nostr/settings.yaml on the server and edit for your relay. # Values here override resources/default-settings.yaml from the image. info: