| Service | vCPU | RAM | Storage |
|---|---|---|---|
| db | 0.5 | 1GB | 5GB |
| redis | 0.25 | 512MB | 2GB |
| relay | 1 | 2GB | 10GB |
| s3 | 0.5 | 1GB | 5GB |
| Total | 2.25 | 4.5GB | 22GB |
A workspace where humans and agents build together, on a relay you own
Source repository ↗Log in to deploy this app.
Log inservices: db: image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73 user: root # postgres' entrypoint starts as root, then drops to `postgres` resources: { cpu: 500m, memory: 1Gi } # A port block is what gives the service a DNS name inside the namespace: # no ports, no Service, and `db` in the relay's DATABASE_URL does not # resolve. `expose: none` keeps it internal. ports: - { name: postgres, container: 5432, protocol: tcp, expose: none } env: POSTGRES_DB: buzz POSTGRES_USER: buzz POSTGRES_PASSWORD: ${DB_PASSWORD} # initdb refuses a non-empty directory, and a fresh ext4 PVC has lost+found PGDATA: /var/lib/postgresql/data/pgdata volumes: - { name: data, path: /var/lib/postgresql/data, size: 5Gi, label: database } scratch: # readOnlyRootFilesystem: the postmaster lock + unix socket need a # writable dir, but not a persistent one - { path: /var/run/postgresql, size: 32Mi } backup: # Runs in its own pod on this image, so it connects to the service over # the network (-h db); the default unix socket exists only inside # postgres' own container. PGPASSWORD comes from the service's env. command: ["sh", "-c", "PGPASSWORD=$POSTGRES_PASSWORD exec pg_dumpall -h db -U buzz"] artifact: buzz.sql redis: image: redis:7-alpine@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf user: root # redis' entrypoint starts as root, then drops to `redis` resources: { cpu: 250m, memory: 512Mi } ports: - { name: redis, container: 6379, protocol: tcp, expose: none } volumes: # RDB snapshots; without a writable /data redis fails its bgsave and then # refuses writes with MISCONF - { name: data, path: /data, size: 2Gi } s3: image: rustfs/rustfs:1.0.0-beta.11@sha256:84ce557a0245a06a9aae5516f55ee0f007fca78d41df356f419306fdc0cb168c user: "10001" # image sets `USER rustfs` (a name); uid 10001 per its Dockerfile resources: { cpu: 500m, memory: 1Gi } ports: - { name: s3, container: 9000, protocol: http, expose: none } env: RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY} RUSTFS_SECRET_KEY: ${S3_SECRET_KEY} RUSTFS_VOLUMES: /data RUSTFS_ADDRESS: ":9000" RUSTFS_CONSOLE_ENABLE: "false" # image default is RUSTFS_OBS_LOG_DIRECTORY=/logs, which is unwritable # under readOnlyRootFilesystem — log to stdout instead RUSTFS_OBS_LOG_DIRECTORY: "" RUSTFS_OBS_USE_STDOUT: "true" RUSTFS_OBS_LOGGER_LEVEL: warn volumes: - { name: blobs, path: /data, size: 5Gi, label: media } backup: volume: blobs relay: image: ghcr.io/block/buzz:latest@sha256:4e31b7c7abb7d00b6f513dc559e58d2b980416f1dc400aa01bcf762cf2989cfc user: "1000" # image sets `USER buzz:buzz` (a name); uid 1000 per its Dockerfile resources: { cpu: 1, memory: 2Gi } depends_on: [db, redis, s3] ports: - { name: http, container: 3000, protocol: http, expose: ingress } # The relay never issues CreateBucket — it probes the object store at # startup and dies on NoSuchBucket. This runs before the relay's own # container, and the kubelet retries it while RustFS is still coming up, so # the relay cannot start before the bucket exists. init: - name: create-media-bucket image: rclone/rclone:1.75.1@sha256:45401ad7410db1d67ffdb58e19059ad20b0d8e0285a60e38bbec55cc1019c7a5 user: "65534" # only talks to the in-namespace S3 service env: RCLONE_CONFIG: /tmp/rclone.conf RCLONE_CONFIG_S3_TYPE: s3 RCLONE_CONFIG_S3_PROVIDER: Other RCLONE_CONFIG_S3_ENDPOINT: "http://s3:9000" RCLONE_CONFIG_S3_REGION: us-east-1 RCLONE_CONFIG_S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY}" RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: "${S3_SECRET_KEY}" command: - sh - -c - | set -e until rclone lsd s3: >/dev/null 2>&1; do echo "waiting for http://s3:9000" sleep 2 done rclone mkdir s3:buzz-media env: # --- identity / public URL --- RELAY_URL: "wss://${HOSTNAME}" # The relay validates this at startup and exits 1 on anything that does # not end with /media — "invalid media config: public_base_url must end # with /media". It is the public prefix it hands out for blobs, # not the host it binds. BUZZ_MEDIA_BASE_URL: "https://${HOSTNAME}/media" BUZZ_BIND_ADDR: "0.0.0.0:3000" BUZZ_RELAY_PRIVATE_KEY: "${BUZZ_RELAY_PRIVATE_KEY}" RELAY_OWNER_PUBKEY: "${owner_pubkey}" # --- backing services (in-namespace DNS) --- DATABASE_URL: "postgres://buzz:${DB_PASSWORD}@db:5432/buzz" REDIS_URL: "redis://redis:6379" BUZZ_AUTO_MIGRATE: "true" # --- object storage --- BUZZ_S3_ENDPOINT: "http://s3:9000" BUZZ_S3_BUCKET: "buzz-media" BUZZ_S3_REGION: "us-east-1" BUZZ_S3_ACCESS_KEY: "${S3_ACCESS_KEY}" BUZZ_S3_SECRET_KEY: "${S3_SECRET_KEY}" # --- access control --- BUZZ_REQUIRE_AUTH_TOKEN: "${require_auth_token}" BUZZ_REQUIRE_RELAY_MEMBERSHIP: "${require_relay_membership}" BUZZ_ALLOW_NIP_OA_AUTH: "${allow_nip_oa_auth}" BUZZ_PUBKEY_ALLOWLIST: "${pubkey_allowlist}" BUZZ_AGE_ATTESTATION_REQUIRED: "${age_attestation_required}" BUZZ_TERMS_OF_SERVICE_MARKDOWN: "${terms_of_service}" BUZZ_PRIVACY_POLICY_MARKDOWN: "${privacy_policy}" BUZZ_AUDIT_ENABLED: "${audit_enabled}" BUZZ_MAX_IMAGE_BYTES: "${max_image_bytes}" BUZZ_MAX_GIF_BYTES: "${max_gif_bytes}" BUZZ_MAX_VIDEO_BYTES: "${max_video_bytes}" BUZZ_MAX_FILE_BYTES: "${max_file_bytes}" BUZZ_MEDIA_UPLOADS_PER_MINUTE: "${media_uploads_per_minute}" # --- git on object storage --- BUZZ_GIT_REPO_PATH: "/var/lib/buzz/git" BUZZ_GIT_PACK_CACHE_PATH: "/var/cache/buzz/git-packs" BUZZ_GIT_HOOK_HMAC_SECRET: "${GIT_HOOK_HMAC_SECRET}" # RustFS serves 503 under the default 32-way conditional-PUT race; the A3 # probe is startup-fatal, so keep the race narrow (verified: 4x1 passes) BUZZ_GIT_PROBE_WRITERS: "4" BUZZ_GIT_PROBE_ROUNDS: "1" BUZZ_GIT_MAX_PACK_BYTES: "${git_max_pack_bytes}" BUZZ_GIT_MAX_REPO_BYTES: "${git_max_repo_bytes}" BUZZ_GIT_MAX_REPOS_PER_PUBKEY: "${git_max_repos_per_pubkey}" BUZZ_SERVE_GIT_WEB_GUI: "${serve_git_web_gui}" # --- capacity / logging --- BUZZ_MAX_CONNECTIONS: "${max_connections}" RUST_LOG: "${log_level}" env_from: extra_env volumes: - { name: git, path: /var/lib/buzz/git, size: 5Gi, label: git repositories } - { name: packs, path: /var/cache/buzz/git-packs, size: 5Gi } secrets: - { name: DB_PASSWORD, generate: password } - { name: S3_ACCESS_KEY, generate: token } - { name: S3_SECRET_KEY, generate: password } - { name: GIT_HOOK_HMAC_SECRET, generate: token } # 32 bytes exactly: this is the relay's Nostr secret key, and the default 24 # is rejected at startup as an invalid secret key. - { name: BUZZ_RELAY_PRIVATE_KEY, generate: token, bytes: 32 } config: - { name: owner_pubkey, label: "Owner pubkey (64-char hex)", type: string, required: true, pattern: "[0-9a-fA-F]{64}" } - { name: require_auth_token, label: "Require auth for every connection", type: bool, default: "true", group: "Access" } - { name: require_relay_membership, label: "Only relay members can use the relay", type: bool, default: "true", group: "Access" } - { name: allow_nip_oa_auth, label: "Allow agent auth (NIP-OA)", type: bool, default: "true", group: "Access" } - { name: pubkey_allowlist, label: "Enforce the pubkey allowlist", type: bool, default: "false", group: "Access" } - { name: age_attestation_required, label: "Require age attestation to join", type: bool, default: "false", group: "Access" } - { name: terms_of_service, label: "Terms of service (markdown)", type: file, default: "", group: "Policies" } - { name: privacy_policy, label: "Privacy policy (markdown)", type: file, default: "", group: "Policies" } - { name: max_image_bytes, label: "Max image upload (bytes)", type: int, default: "52428800", pattern: "[1-9][0-9]*", group: "Media limits" } - { name: max_gif_bytes, label: "Max GIF upload (bytes)", type: int, default: "10485760", pattern: "[1-9][0-9]*", group: "Media limits" } - { name: max_video_bytes, label: "Max video upload (bytes)", type: int, default: "524288000", pattern: "[1-9][0-9]*", group: "Media limits" } - { name: max_file_bytes, label: "Max file upload (bytes)", type: int, default: "104857600", pattern: "[1-9][0-9]*", group: "Media limits" } - { name: media_uploads_per_minute, label: "Media uploads per minute per user", type: int, default: "30", pattern: "[1-9][0-9]*", group: "Media limits" } - { name: git_max_pack_bytes, label: "Max git push (bytes)", type: int, default: "524288000", pattern: "[1-9][0-9]*", group: "Git" } - { name: git_max_repo_bytes, label: "Max git repository size (bytes)", type: int, default: "1048576000", pattern: "[1-9][0-9]*", group: "Git" } - { name: git_max_repos_per_pubkey, label: "Max git repositories per user", type: int, default: "100", pattern: "[1-9][0-9]*", group: "Git" } - { name: serve_git_web_gui, label: "Serve the git web viewer", type: bool, default: "false", group: "Git" } - { name: audit_enabled, label: "Audit log", type: bool, default: "true", group: "Advanced" } - { name: max_connections, label: "Max connections", type: int, default: "2000", pattern: "[1-9][0-9]*", group: "Advanced" } - { name: log_level, label: "Log level (RUST_LOG)", type: string, default: "info", pattern: "[A-Za-z0-9_:=,.-]+", group: "Advanced" } - { name: extra_env, label: "Advanced: extra environment variables (KEY=value per line)", type: file, default: "", group: "Advanced" } # Daily at 03:00 UTC, keeping a week of restore points. Each run captures # every service that declares a `backup:` method above, at one point in time. backup: { schedule: "0 3 * * *", retention: 7 }