diff --git a/deploy/railway/README.md b/deploy/railway/README.md new file mode 100644 index 000000000..60232de7c --- /dev/null +++ b/deploy/railway/README.md @@ -0,0 +1,59 @@ +# Buzz on Railway + +This directory contains Buzz's first-party Railway template draft. It is intentionally scoped to Railway only; the repository README deploy button should be wired after the public image and automatic migrations PRs land. + +```markdown +[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/buzz?referralCode=buzz) +``` + +## Template shape + +`railway.template.json` models the current full Buzz deployment as six Railway services: + +- `Buzz` (`ghcr.io/block/buzz:main`) — the relay and bundled web UI. `:main` is the rolling pre-release image published from the repository's default branch; production operators should pin `:sha-<7>` or a semver tag once releases exist. +- `Postgres` — persistent database volume with daily backups requested in the template. +- `Redis` — authenticated persistent pub/sub/cache service. +- `Typesense` — persistent search service. +- `MinIO` — S3-compatible media/object storage. +- `MinIO Init` — idempotently creates the `buzz-media` bucket. + +The only required user input is `RELAY_OWNER_PUBKEY`, a 64-character hex Nostr public key. Railway generates the relay key, database password, Redis password, Typesense key, MinIO password, and git hook HMAC secret with `${{secret(...)}}`. + +## Buzz-specific defaults + +The template uses the environment names read by `crates/buzz-relay/src/config.rs`. The app router itself exposes `/_readiness`, so Railway can health-check the public service without using Buzz's separate health-only port: + +- `RELAY_URL=wss://${{RAILWAY_PUBLIC_DOMAIN}}` +- `RELAY_OWNER_PUBKEY=` +- `DATABASE_URL=${{Postgres.DATABASE_URL}}` +- `REDIS_URL=${{Redis.REDIS_URL}}` +- `TYPESENSE_URL=http://${{Typesense.RAILWAY_PRIVATE_DOMAIN}}:${{Typesense.PORT}}` +- `TYPESENSE_API_KEY=${{Typesense.TYPESENSE_API_KEY}}` +- `BUZZ_S3_*` from the MinIO service; MinIO also sets `MINIO_DOMAIN=${{RAILWAY_PRIVATE_DOMAIN}}` so path-style S3 requests work on Railway private networking. +- `BUZZ_GIT_REPO_PATH=/data/git` +- `BUZZ_REQUIRE_AUTH_TOKEN=true` +- `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true` +- `BUZZ_ALLOW_NIP_OA_AUTH=true` + +Railway terminates TLS, so the public relay URL is `wss://` and CORS/media URLs use `https://`. + +## Validation status + +This template is mechanically valid JSON and follows the v2 template mechanics observed in Railway's Plausible, NocoDB, and Typesense templates: service refs, generated secrets, `serviceDomains`, `volumeMounts`, and app health checks. + +End-to-end click-through validation is intentionally blocked until: + +1. `ghcr.io/block/buzz:main` is publicly published by the image pipeline. +2. Buzz owns fresh-database migrations at startup or through the same image. + +Until both land, this template should be treated as first-party deploy wiring, not a proven production install. + +## Operational notes for users + +Back up these values and volumes before relying on a Railway deployment: + +- The owner private key corresponding to `RELAY_OWNER_PUBKEY` — Railway only stores the public key. +- `BUZZ_RELAY_PRIVATE_KEY` and `BUZZ_GIT_HOOK_HMAC_SECRET`. +- Postgres data. +- MinIO media/object data. +- The Buzz `/data` volume that stores git name-reservation state. diff --git a/deploy/railway/railway.template.json b/deploy/railway/railway.template.json new file mode 100644 index 000000000..49a45f6b3 --- /dev/null +++ b/deploy/railway/railway.template.json @@ -0,0 +1,187 @@ +{ + "$schema": "https://railway.com/railway.schema.json", + "name": "Buzz", + "description": "Self-host a Buzz Nostr relay with Postgres, Redis, Typesense, and S3-compatible media storage.", + "services": { + "buzz": { + "name": "Buzz", + "source": { + "image": "ghcr.io/block/buzz:main" + }, + "deploy": { + "healthcheckPath": "/_readiness", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + }, + "networking": { + "serviceDomains": { + "buzz": {} + } + }, + "variables": { + "BUZZ_BIND_ADDR": "0.0.0.0:${{PORT}}", + "BUZZ_METRICS_PORT": "9102", + "BUZZ_RELAY_PRIVATE_KEY": "${{secret(64, 'abcdef0123456789')}}", + "BUZZ_REQUIRE_AUTH_TOKEN": "true", + "BUZZ_REQUIRE_RELAY_MEMBERSHIP": "true", + "BUZZ_ALLOW_NIP_OA_AUTH": "true", + "BUZZ_CORS_ORIGINS": "https://${{RAILWAY_PUBLIC_DOMAIN}}", + "BUZZ_GIT_REPO_PATH": "/data/git", + "BUZZ_GIT_HOOK_HMAC_SECRET": "${{secret(64, 'abcdef0123456789')}}", + "BUZZ_GIT_CONFORMANCE_PROBE": "true", + "BUZZ_S3_ENDPOINT": "http://${{MinIO.RAILWAY_PRIVATE_DOMAIN}}:${{MinIO.PORT}}", + "BUZZ_S3_ACCESS_KEY": "${{MinIO.MINIO_ROOT_USER}}", + "BUZZ_S3_SECRET_KEY": "${{MinIO.MINIO_ROOT_PASSWORD}}", + "BUZZ_S3_BUCKET": "buzz-media", + "BUZZ_MEDIA_BASE_URL": "https://${{RAILWAY_PUBLIC_DOMAIN}}/media", + "DATABASE_URL": "${{Postgres.DATABASE_URL}}", + "REDIS_URL": "${{Redis.REDIS_URL}}", + "RELAY_OWNER_PUBKEY": { + "description": "Required: 64-character hex Nostr public key for the relay owner. Generate or back up the matching private key outside Railway.", + "isOptional": false + }, + "RELAY_URL": "wss://${{RAILWAY_PUBLIC_DOMAIN}}", + "TYPESENSE_API_KEY": "${{Typesense.TYPESENSE_API_KEY}}", + "TYPESENSE_URL": "http://${{Typesense.RAILWAY_PRIVATE_DOMAIN}}:${{Typesense.PORT}}" + }, + "volumeMounts": [ + { + "mountPath": "/data", + "volumeName": "buzz-data" + } + ] + }, + "postgres": { + "name": "Postgres", + "source": { + "image": "ghcr.io/railwayapp-templates/postgres-ssl:17" + }, + "deploy": { + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + }, + "variables": { + "PGDATA": "/var/lib/postgresql/data", + "POSTGRES_DB": "buzz", + "POSTGRES_PASSWORD": "${{secret(32)}}", + "POSTGRES_USER": "buzz" + }, + "volumeMounts": [ + { + "mountPath": "/var/lib/postgresql/data", + "volumeName": "postgres-data", + "backupSchedules": [ + "DAILY" + ] + } + ] + }, + "redis": { + "name": "Redis", + "source": { + "image": "redis:8-alpine" + }, + "deploy": { + "startCommand": "/bin/sh -c \"rm -rf $RAILWAY_VOLUME_MOUNT_PATH/lost+found && exec redis-server --requirepass $REDIS_PASSWORD --appendonly yes --dir $RAILWAY_VOLUME_MOUNT_PATH\"", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + }, + "variables": { + "REDIS_PASSWORD": "${{secret(32)}}", + "REDIS_URL": "redis://:${{REDIS_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:6379" + }, + "volumeMounts": [ + { + "mountPath": "/data", + "volumeName": "redis-data" + } + ] + }, + "typesense": { + "name": "Typesense", + "source": { + "image": "typesense/typesense:30.2" + }, + "deploy": { + "startCommand": "typesense-server --data-dir $RAILWAY_VOLUME_MOUNT_PATH --api-key $TYPESENSE_API_KEY --enable-cors", + "healthcheckPath": "/health", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + }, + "variables": { + "PORT": "8108", + "TYPESENSE_API_KEY": "${{secret(32)}}" + }, + "volumeMounts": [ + { + "mountPath": "/data", + "volumeName": "typesense-data" + } + ] + }, + "minio": { + "name": "MinIO", + "source": { + "image": "minio/minio:RELEASE.2025-09-07T16-13-09Z" + }, + "deploy": { + "startCommand": "server $RAILWAY_VOLUME_MOUNT_PATH --address :$PORT", + "healthcheckPath": "/minio/health/live", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + }, + "variables": { + "MINIO_BROWSER": "off", + "MINIO_ROOT_PASSWORD": "${{secret(32)}}", + "MINIO_ROOT_USER": "buzz", + "PORT": "9000", + "MINIO_DOMAIN": "${{RAILWAY_PRIVATE_DOMAIN}}" + }, + "volumeMounts": [ + { + "mountPath": "/data", + "volumeName": "minio-data" + } + ] + }, + "minio-init": { + "name": "MinIO Init", + "source": { + "image": "minio/mc:RELEASE.2025-08-13T08-35-41Z" + }, + "deploy": { + "startCommand": "/bin/sh -c \"until mc alias set local http://${{MinIO.RAILWAY_PRIVATE_DOMAIN}}:${{MinIO.PORT}} ${{MinIO.MINIO_ROOT_USER}} ${{MinIO.MINIO_ROOT_PASSWORD}}; do sleep 2; done; mc mb --ignore-existing local/buzz-media; mc anonymous set none local/buzz-media\"", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + } + } + }, + "canvasConfig": { + "services": { + "buzz": { + "x": 0, + "y": 0 + }, + "postgres": { + "x": -280, + "y": 180 + }, + "redis": { + "x": 0, + "y": 180 + }, + "typesense": { + "x": 280, + "y": 180 + }, + "minio": { + "x": 560, + "y": 180 + }, + "minio-init": { + "x": 560, + "y": 360 + } + } + } +}