LNVPS
‹ Back to apps

Buzz Relay

€4/month
ServicevCPURAMStorage
db0.51GB5GB
redis0.25512MB2GB
relay12GB10GB
s30.51GB5GB
Total2.254.5GB22GB

A workspace where humans and agents build together, on a relay you own

Source repository ↗
Deploy

Log in to deploy this app.

Log in
Compose
services:
  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 }