Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/deploy-production-compose.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions deploy/Dockerfile.migrate
Original file line number Diff line number Diff line change
@@ -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"]
133 changes: 133 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Production deployment

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.

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/
├── 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 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` 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 |

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.

## Deploy

```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 compose up -d
docker compose logs -f nostream-migrate
docker compose logs -f nostream
```

## 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`.
91 changes: 91 additions & 0 deletions deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -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:
23 changes: 23 additions & 0 deletions deploy/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copy to .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
26 changes: 26 additions & 0 deletions deploy/settings.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copy to .nostr/settings.yaml on the server and edit for your relay.
# 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
Loading