diff --git a/deploy/compose/.env.example b/deploy/compose/.env.example new file mode 100644 index 000000000..cebe879da --- /dev/null +++ b/deploy/compose/.env.example @@ -0,0 +1,52 @@ +# Buzz production Docker Compose environment. +# Copy to .env and replace every CHANGE_ME value before running. +# The bootstrap script should generate this file for normal users. + +# Image published by the public image pipeline. Use `:main` for pre-release testing. Pin `:sha-<7>` or a semver release tag for production. +BUZZ_IMAGE=ghcr.io/block/buzz:main + +# Public host name. Used by compose.caddy.yml and URL-derived settings below. +BUZZ_DOMAIN=buzz.example.com +RELAY_URL=wss://buzz.example.com +BUZZ_MEDIA_BASE_URL=https://buzz.example.com/media +BUZZ_MEDIA_SERVER_DOMAIN=buzz.example.com +BUZZ_CORS_ORIGINS=https://buzz.example.com + +# Production defaults. Closed relay mode requires RELAY_OWNER_PUBKEY and a stable relay key. +BUZZ_REQUIRE_AUTH_TOKEN=true +BUZZ_REQUIRE_RELAY_MEMBERSHIP=true +BUZZ_ALLOW_NIP_OA_AUTH=true +BUZZ_AUTO_MIGRATE=true +BUZZ_GIT_CONFORMANCE_PROBE=true +RUST_LOG=buzz_relay=info,buzz_db=info,buzz_auth=info,buzz_pubsub=info,tower_http=info + +# Owner identity. Set to a 64-character hex Nostr pubkey. +RELAY_OWNER_PUBKEY=CHANGE_ME_OWNER_PUBKEY_HEX + +# Stable secrets. Generate once, keep in .env, and back up securely. +BUZZ_RELAY_PRIVATE_KEY=CHANGE_ME_64_HEX_PRIVATE_KEY +BUZZ_GIT_HOOK_HMAC_SECRET=CHANGE_ME_RANDOM_64_HEX +POSTGRES_DB=buzz +POSTGRES_USER=buzz +POSTGRES_PASSWORD=CHANGE_ME_RANDOM_PASSWORD +REDIS_PASSWORD=CHANGE_ME_RANDOM_PASSWORD +TYPESENSE_API_KEY=CHANGE_ME_RANDOM_API_KEY +BUZZ_S3_ACCESS_KEY=CHANGE_ME_RANDOM_ACCESS_KEY +BUZZ_S3_SECRET_KEY=CHANGE_ME_RANDOM_SECRET_KEY +BUZZ_S3_BUCKET=buzz-media + +# Optional host ports. Base compose publishes the relay directly on BUZZ_HTTP_PORT. +BUZZ_HTTP_PORT=3000 + +# Caddy host ports. Only used with compose.caddy.yml. +CADDY_HTTP_PORT=80 +CADDY_HTTPS_PORT=443 + +# Dev override ports. Only used with compose.dev.yml. +POSTGRES_PORT=5432 +REDIS_PORT=6379 +TYPESENSE_PORT=8108 +MINIO_API_PORT=9000 +MINIO_CONSOLE_PORT=9001 +ADMINER_PORT=8082 +PROMETHEUS_PORT=9090 diff --git a/deploy/compose/Caddyfile b/deploy/compose/Caddyfile new file mode 100644 index 000000000..205cf4c5b --- /dev/null +++ b/deploy/compose/Caddyfile @@ -0,0 +1,5 @@ +{$BUZZ_DOMAIN} { + encode zstd gzip + + reverse_proxy relay:3000 +} diff --git a/deploy/compose/README.md b/deploy/compose/README.md new file mode 100644 index 000000000..e4238dc5e --- /dev/null +++ b/deploy/compose/README.md @@ -0,0 +1,57 @@ +# Buzz Docker Compose deployment + +This is the single-node/VPS deployment bundle. It is intentionally separate from +the root `docker-compose.yml`, which remains local development infrastructure. + +## Quick start + +```bash +cd deploy/compose +cp .env.example .env +$EDITOR .env # replace every CHANGE_ME value +./run.sh start +``` + +For a public VPS with automatic Let's Encrypt certificates: + +```bash +cd deploy/compose +BUZZ_COMPOSE_TLS=true ./run.sh start +``` + +The bootstrap script should eventually replace manual `.env` editing for normal +users. It is responsible for generating stable secrets and, optionally, an owner +keypair. + +## Production notes + +- Requires Docker Compose v2.24.4 or newer; the TLS override uses Compose's + `!reset` tag to remove the direct relay port when Caddy terminates HTTPS. +- Default `BUZZ_IMAGE` tracks `ghcr.io/block/buzz:main` for early testing. Pin it to `ghcr.io/block/buzz:sha-<7>` or a semver release tag for production once available. +- Keep `BUZZ_RELAY_PRIVATE_KEY`, `BUZZ_GIT_HOOK_HMAC_SECRET`, database/Redis, + Typesense, and S3 secrets stable across restarts. +- `RELAY_OWNER_PUBKEY` is intentionally not prefixed with `BUZZ_`; it must be a + 64-character hex Nostr pubkey when closed relay mode is enabled. +- `BUZZ_AUTO_MIGRATE=true` requires an image that includes embedded SQLx + migrations. Do not share this quick start for a fresh public install until PR + #988 is merged and `ghcr.io/block/buzz:main` has been rebuilt from it. Before + then, this bundle is only suitable for instances whose database schema has + already been applied. +- The stack uses Postgres, Redis, Typesense, MinIO, and a git data volume because + those are real Buzz dependencies today. Minimal mode can simplify this later. + +Run `./run.sh backup-hint` for the backup checklist. + +## Validation + +Before sharing an install link publicly, verify a fresh install with: + +```bash +cd deploy/compose +cp .env.example .env +$EDITOR .env +./run.sh config +./run.sh start +curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness" +./run.sh status +``` diff --git a/deploy/compose/compose.caddy.yml b/deploy/compose/compose.caddy.yml new file mode 100644 index 000000000..c7dcbf106 --- /dev/null +++ b/deploy/compose/compose.caddy.yml @@ -0,0 +1,29 @@ +services: + relay: + ports: !reset [] + + caddy: + image: caddy:2-alpine + depends_on: + relay: + condition: service_healthy + environment: + BUZZ_DOMAIN: ${BUZZ_DOMAIN:?set BUZZ_DOMAIN} + ports: + - "${CADDY_HTTP_PORT:-80}:80" + - "${CADDY_HTTPS_PORT:-443}:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - buzz-caddy-data:/data + - buzz-caddy-config:/config + restart: unless-stopped + networks: + - buzz-net + +volumes: + buzz-caddy-data: + labels: + com.buzz.volume: caddy-data + buzz-caddy-config: + labels: + com.buzz.volume: caddy-config diff --git a/deploy/compose/compose.dev.yml b/deploy/compose/compose.dev.yml new file mode 100644 index 000000000..258553a72 --- /dev/null +++ b/deploy/compose/compose.dev.yml @@ -0,0 +1,52 @@ +services: + postgres: + ports: + - "${POSTGRES_PORT:-5432}:5432" + + redis: + ports: + - "${REDIS_PORT:-6379}:6379" + + typesense: + environment: + TYPESENSE_ENABLE_CORS: "true" + ports: + - "${TYPESENSE_PORT:-8108}:8108" + + minio: + ports: + - "${MINIO_API_PORT:-9000}:9000" + - "${MINIO_CONSOLE_PORT:-9001}:9001" + + adminer: + image: adminer:latest + container_name: buzz-adminer + depends_on: + postgres: + condition: service_healthy + environment: + ADMINER_DEFAULT_SERVER: postgres + ports: + - "${ADMINER_PORT:-8082}:8080" + restart: unless-stopped + networks: + - buzz-net + + prometheus: + image: prom/prometheus:latest + container_name: buzz-prometheus + volumes: + - ../../prometheus.yml:/etc/prometheus/prometheus.yml:ro + - buzz-prometheus-data:/prometheus + ports: + - "${PROMETHEUS_PORT:-9090}:9090" + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + networks: + - buzz-net + +volumes: + buzz-prometheus-data: + labels: + com.buzz.volume: prometheus diff --git a/deploy/compose/compose.yml b/deploy/compose/compose.yml new file mode 100644 index 000000000..859ed6585 --- /dev/null +++ b/deploy/compose/compose.yml @@ -0,0 +1,168 @@ +name: buzz-prod + +services: + relay: + image: ${BUZZ_IMAGE:-ghcr.io/block/buzz:main} + env_file: + - .env + environment: + BUZZ_BIND_ADDR: 0.0.0.0:3000 + BUZZ_HEALTH_PORT: "8080" + BUZZ_METRICS_PORT: "9102" + DATABASE_URL: postgres://${POSTGRES_USER:-buzz}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-buzz} + REDIS_URL: redis://:${REDIS_PASSWORD:?set REDIS_PASSWORD}@redis:6379 + TYPESENSE_URL: http://typesense:8108 + BUZZ_S3_ENDPOINT: http://minio:9000 + BUZZ_S3_ACCESS_KEY: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY} + BUZZ_S3_SECRET_KEY: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY} + BUZZ_S3_BUCKET: ${BUZZ_S3_BUCKET:-buzz-media} + BUZZ_GIT_REPO_PATH: /data/git + BUZZ_AUTO_MIGRATE: ${BUZZ_AUTO_MIGRATE:-true} + BUZZ_GIT_CONFORMANCE_PROBE: ${BUZZ_GIT_CONFORMANCE_PROBE:-true} + ports: + - "${BUZZ_HTTP_PORT:-3000}:3000" + volumes: + - buzz-git-data:/data/git + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + typesense: + condition: service_healthy + minio: + condition: service_healthy + minio-init: + condition: service_completed_successfully + # Probe /_readiness over /dev/tcp because the runtime image has bash but no curl/wget/socat. + healthcheck: + test: + [ + "CMD-SHELL", + "bash -ec 'exec 3<>/dev/tcp/127.0.0.1/8080; printf \"GET /_readiness HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n\" >&3; grep -q \"200 OK\" <&3'", + ] + interval: 10s + timeout: 3s + retries: 12 + start_period: 30s + restart: unless-stopped + networks: + - buzz-net + + postgres: + image: postgres:18-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-buzz} + POSTGRES_USER: ${POSTGRES_USER:-buzz} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - buzz-postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + restart: unless-stopped + networks: + - buzz-net + + redis: + image: redis:8-alpine + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?set REDIS_PASSWORD}"] + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:?set REDIS_PASSWORD} + volumes: + - buzz-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 5s + restart: unless-stopped + networks: + - buzz-net + + typesense: + image: typesense/typesense:30.2 + environment: + TYPESENSE_DATA_DIR: /data + TYPESENSE_API_KEY: ${TYPESENSE_API_KEY:?set TYPESENSE_API_KEY} + TYPESENSE_ENABLE_CORS: "false" + volumes: + - buzz-typesense-data:/data + healthcheck: + test: + [ + "CMD-SHELL", + "bash -c 'exec 3<>/dev/tcp/127.0.0.1/8108; printf \"GET /health HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n\" >&3; cat <&3 | grep -q ok'", + ] + interval: 10s + timeout: 5s + retries: 12 + start_period: 15s + restart: unless-stopped + networks: + - buzz-net + + minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY} + volumes: + - buzz-minio-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + restart: unless-stopped + networks: + - buzz-net + + minio-init: + image: minio/mc:RELEASE.2025-08-13T08-35-41Z + depends_on: + minio: + condition: service_healthy + environment: + BUZZ_S3_ACCESS_KEY: ${BUZZ_S3_ACCESS_KEY:?set BUZZ_S3_ACCESS_KEY} + BUZZ_S3_SECRET_KEY: ${BUZZ_S3_SECRET_KEY:?set BUZZ_S3_SECRET_KEY} + BUZZ_S3_BUCKET: ${BUZZ_S3_BUCKET:-buzz-media} + entrypoint: > + /bin/sh -euc ' + mc alias set local http://minio:9000 "$${BUZZ_S3_ACCESS_KEY}" "$${BUZZ_S3_SECRET_KEY}" + mc mb --ignore-existing "local/$${BUZZ_S3_BUCKET}" + mc anonymous set none "local/$${BUZZ_S3_BUCKET}" + ' + restart: "no" + networks: + - buzz-net + +volumes: + buzz-postgres-data: + labels: + com.buzz.volume: postgres + buzz-redis-data: + labels: + com.buzz.volume: redis + buzz-typesense-data: + labels: + com.buzz.volume: typesense + buzz-minio-data: + labels: + com.buzz.volume: minio + buzz-git-data: + labels: + com.buzz.volume: git + +networks: + buzz-net: + driver: bridge + labels: + com.buzz.network: production diff --git a/deploy/compose/run.sh b/deploy/compose/run.sh new file mode 100755 index 000000000..0c716679b --- /dev/null +++ b/deploy/compose/run.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +COMPOSE_FILES=(-f compose.yml) +if [[ "${BUZZ_COMPOSE_TLS:-false}" == "true" ]]; then + COMPOSE_FILES+=(-f compose.caddy.yml) +fi +if [[ "${BUZZ_COMPOSE_DEV:-false}" == "true" ]]; then + COMPOSE_FILES+=(-f compose.dev.yml) +fi + +compose() { + docker compose --env-file .env "${COMPOSE_FILES[@]}" "$@" +} + +require_env() { + if [[ ! -f .env ]]; then + cat >&2 <<'MSG' +Missing deploy/compose/.env. + +Copy .env.example to .env and replace every CHANGE_ME value, or run the bootstrap +script once it lands. Do not start production with generated secrets missing. +MSG + exit 1 + fi + if grep -Eq '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=.*CHANGE_ME' .env; then + cat >&2 <<'MSG' +deploy/compose/.env still contains CHANGE_ME placeholders. +Generate stable secrets first; these values must not rotate on restart. +MSG + exit 1 + fi +} + +backup_hint() { + cat <<'MSG' +Back up these before upgrades and on a regular schedule: + +- deploy/compose/.env, especially BUZZ_RELAY_PRIVATE_KEY, DB/Redis/Typesense/S3 secrets, and BUZZ_GIT_HOOK_HMAC_SECRET +- The owner private key if bootstrap generated one for RELAY_OWNER_PUBKEY +- Postgres data (prefer pg_dump or a quiesced volume snapshot) +- MinIO/S3 bucket contents for media and git objects +- buzz-git-data volume (BUZZ_GIT_REPO_PATH=/data/git) +- Caddy data/config volumes if using compose.caddy.yml + +Keep Postgres + object/git state snapshots from the same maintenance window. +MSG +} + +case "${1:-help}" in + start|up) + require_env + compose up -d --wait + ;; + stop|down) + compose down + ;; + restart) + require_env + compose up -d --wait --force-recreate relay + ;; + pull) + require_env + compose pull + ;; + upgrade) + require_env + compose pull + compose up -d --wait + backup_hint + ;; + logs) + shift || true + compose logs -f "${@:-relay}" + ;; + status|ps) + compose ps + ;; + config) + require_env + compose config + ;; + backup-hint) + backup_hint + ;; + help|-h|--help) + cat <<'MSG' +Usage: ./run.sh + +Commands: + start Start Buzz with docker compose up -d --wait + stop Stop containers without deleting volumes + restart Recreate the relay after env/image changes + pull Pull configured images + upgrade Pull and restart, then print backup reminders + logs [svc] Follow logs (default: relay) + status Show compose service status + config Render merged compose config + backup-hint Print the production backup checklist + +Environment switches: + BUZZ_COMPOSE_TLS=true Include compose.caddy.yml for automatic HTTPS + BUZZ_COMPOSE_DEV=true Include compose.dev.yml for local admin ports/tools +MSG + ;; + *) + echo "Unknown command: $1" >&2 + echo "Run ./run.sh help" >&2 + exit 1 + ;; +esac