Skip to content
Open
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
59 changes: 59 additions & 0 deletions deploy/railway/README.md
Original file line number Diff line number Diff line change
@@ -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=<required user input>`
- `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.
187 changes: 187 additions & 0 deletions deploy/railway/railway.template.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}