version: '3.8' networks: public: external: true internal: external: true volumes: stamp-monitor-data: driver: local postgres-data: driver: local redis-data: driver: local bee-data: driver: local pss-key: driver: local secrets: stamp_monitor_admin_token: external: true services: # Watches this node's postage batches and renews them before they lapse. # # Added after the gateway went down because its batch fell under # minTtlDays and the top-up it then attempted (64.64 xBZZ on a depth-24 # batch) exceeded the Bee wallet — ensureManagedStamp threw, and with it # every route that boots the gateway context. Nothing was watching. # # Runs HERE rather than reading the chain from the homelab because bucket # utilisation is not on-chain: stamps are off-chain signatures, so only a # Bee node that saw them knows how full a batch is. Sitting next to prod_bee # is what makes fullness, the bucket map and batch_full alerts possible. stamp-monitor: image: ghcr.io/ffaerber/swarm-stamp-monitor@sha256:5125b63fc430e2f5c9eb97bb5cc6fb60316569468f4e3e6819ddb6ae6809cd09 networks: [internal, public] volumes: - stamp-monitor-data:/data secrets: - stamp_monitor_admin_token env_file: - .env environment: BEE_URL: http://bee:1633 DB_PATH: /data/monitor.sqlite # Read from a swarm secret, not an interpolated env var. The # ${STAMP_MONITOR_ADMIN_TOKEN} form rendered correctly under `docker # stack config` and still deployed EMPTY, which disables the admin API # (503) while the service otherwise looks healthy — a dashboard that # loads and can show nothing. A secret is mounted as a file and needs no # substitution, so there is nothing left to silently resolve to "". ADMIN_TOKEN_FILE: /run/secrets/stamp_monitor_admin_token # Armed: the whole point is that nothing renewed the batch last time. AUTO_TOPUP_ENABLED: "true" DRY_RUN: "false" TOPUP_WHEN_TTL_BELOW_DAYS: "14" TOPUP_TARGET_TTL_DAYS: "60" # 8 xBZZ is chosen, not arbitrary. A depth-20 top-up from 14d to 60d # costs 6.19 and passes; the abandoned depth-25 batch would cost 129.28 # and is refused by this cap alone. The cap is the guard that stops the # monitor reviving a batch we deliberately let expire. MAX_TOPUP_BZZ_PER_BATCH: "8" MAX_TOPUP_BZZ_PER_DAY: "15" MIN_WALLET_BZZ: "10" MIN_WALLET_XDAI: "0.2" # OFF. The gateway re-stamps an identical chunk, so one bucket always # trends full while the batch holds ~2 MB. Diluting that halves the TTL # and doubles the monthly cost for no capacity gained — it is what took # this batch from depth 24 to 25 and 129 xBZZ/month. On a MUTABLE batch # a full bucket simply recycles its own duplicates, which is harmless. DILUTE_ENABLED: "false" PRICE_ENABLED: "true" deploy: replicas: 1 restart_policy: condition: any resources: limits: cpus: "0.30" memory: 256M # Published at bee.t4t-gateway.com. The gateway's own /bee page shows a # summary; this is the full view — batch fullness, the bucket map, wallet # and chequebook runway, and the action ledger. # # Safe to publish because the app fails CLOSED, not because the network # protects it: every /api/admin route requires an x-admin-token and # returns 503 when ADMIN_TOKEN is unset, so a misconfigured deploy is a # visible outage rather than an open door onto endpoints that buy postage. # The Bee node itself stays unpublished — reaching it through here still # needs that token, which is the point of putting the monitor in front. labels: - "traefik.enable=true" - "traefik.docker.network=public" - "traefik.constraint-label=public" - "traefik.http.routers.stamp-monitor.rule=Host(`bee.t4t-gateway.com`)" - "traefik.http.routers.stamp-monitor.entrypoints=websecure" - "traefik.http.routers.stamp-monitor.tls.certresolver=le" - "traefik.http.services.stamp-monitor.loadbalancer.server.port=3000" - "traefik.http.routers.stamp-monitor.middlewares=redirect-to-https" frontend: image: ghcr.io/token-for-token/hosted-gateway-frontend:main networks: - public deploy: replicas: 2 update_config: parallelism: 1 order: start-first resources: limits: memory: 256M reservations: memory: 128M labels: - "traefik.enable=true" - "traefik.docker.network=public" - "traefik.constraint-label=public" - "traefik.http.routers.frontend.rule=Host(`t4t-gateway.com`) || Host(`www.t4t-gateway.com`)" - "traefik.http.routers.frontend.entrypoints=websecure" - "traefik.http.routers.frontend.tls.certresolver=le" - "traefik.http.services.frontend.loadbalancer.server.port=3000" - "traefik.http.routers.frontend.middlewares=redirect-to-https" logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" api: image: ghcr.io/token-for-token/hosted-gateway:main command: ["bun", "run", "src/api.ts"] env_file: - .env environment: DATABASE_URL: postgresql://hosted:hosted@postgres:5432/hosted_gateway?schema=public REDIS_HOST: redis REDIS_PORT: 6379 BEE_API_URL: http://bee:1633 HOSTED_GATEWAY_PORT: 8080 PSS_KEY_PATH: /data/pss.key volumes: # Shared PSS key file — all replicas use the same identity so providers' # PSS replies can be decrypted regardless of which replica handled the # initial request. - pss-key:/data networks: - public - internal deploy: replicas: 2 update_config: parallelism: 1 order: start-first resources: limits: memory: 512M reservations: memory: 512M labels: - "traefik.enable=true" - "traefik.docker.network=public" - "traefik.constraint-label=public" - "traefik.http.routers.api.rule=Host(`api.t4t-gateway.com`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.routers.api.tls.certresolver=le" - "traefik.http.services.api.loadbalancer.server.port=8080" - "traefik.http.routers.api.middlewares=redirect-to-https" logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" worker: image: ghcr.io/token-for-token/hosted-gateway:main command: ["bun", "run", "src/worker.ts"] env_file: - .env environment: DATABASE_URL: postgresql://hosted:hosted@postgres:5432/hosted_gateway?schema=public REDIS_HOST: redis REDIS_PORT: 6379 BEE_API_URL: http://bee:1633 PSS_KEY_PATH: /data/pss.key volumes: - pss-key:/data networks: - internal deploy: replicas: 1 update_config: parallelism: 1 order: start-first resources: limits: memory: 512M reservations: memory: 512M logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" prisma: image: ghcr.io/token-for-token/hosted-gateway:main # ln -sf /bin/true /usr/local/bin/xdg-open → Prisma 7's studio CLI hard- # codes a browser-launch via xdg-open and bubbles the spawn-ENOENT to # process exit. There's no --browser flag any more, so stub it out. command: ["sh", "-c", "ln -sf /bin/true /usr/local/bin/xdg-open && bunx prisma migrate deploy && bunx prisma studio --port 5555"] env_file: - .env environment: DATABASE_URL: postgresql://hosted:hosted@postgres:5432/hosted_gateway?schema=public networks: - internal - public deploy: replicas: 1 restart_policy: condition: on-failure resources: limits: memory: 512M reservations: memory: 256M labels: - "traefik.enable=true" - "traefik.docker.network=public" - "traefik.constraint-label=public" - "traefik.http.routers.prisma.rule=Host(`prisma.t4t-gateway.com`)" - "traefik.http.routers.prisma.entrypoints=websecure" - "traefik.http.routers.prisma.tls.certresolver=le" - "traefik.http.services.prisma.loadbalancer.server.port=5555" - "traefik.http.routers.prisma.middlewares=admin-auth" logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" postgres: image: postgres:16 networks: - internal environment: POSTGRES_USER: hosted POSTGRES_PASSWORD: hosted POSTGRES_DB: hosted_gateway volumes: - postgres-data:/var/lib/postgresql/data deploy: resources: limits: memory: 512M reservations: memory: 512M logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" redis: image: redis:7 networks: - internal volumes: - redis-data:/data deploy: resources: limits: memory: 128M reservations: memory: 64M logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" bee: # Pinned to 2.8.1 — matches the homelab Bee. Both speak the v15 # handshake. Homelab uses a fresh data dir + --target-neighborhood # to keep the same swarmOverlay as its on-chain provider registration. # # Moved off 2.8.0 because that build omits utilizationRatio from /stamps. # The field is not optional in practice: without it a batch's fullness is # unknown, and "unknown" read as 0% — a batch sitting at 215 of 256 # reported as empty, which is the one direction that fails silently. # 2.8.1 sends it. (stamp-monitor now derives it either way, but the node # should not be the thing lying.) # # Digest-pinned so a re-deploy cannot silently pick up a re-tagged image; # this is a node holding a chequebook and a stake. image: ethersphere/bee:2.8.1@sha256:1a7eeab7fc2725132716abd1aa50ceafb83ece59f171e4ea11deefc773e73a57 # NOTE: BEE_BLOCKCHAIN_RPC_ENDPOINT env var is silently ignored — pass # the flag explicitly. Without it Bee logs "chain backend disabled". command: - start - --blockchain-rpc-endpoint=https://rpc.gnosischain.com env_file: - .env environment: BEE_API_ADDR: ":1633" BEE_P2P_ADDR: ":1634" # Tell Bee its public-facing libp2p address so the autonat check # passes and peers learn to dial our node back. Without this Bee # advertises only docker-bridge IPs (172.x / 127.0.0.1) and stays # marked "Private" — PSS forwarding gets blackholed. BEE_NAT_ADDR: "195.35.25.26:1634" BEE_VERBOSITY: "info" BEE_FULL_NODE: "true" BEE_SWAP_ENABLE: "true" BEE_NETWORK_ID: "1" BEE_WELCOME_MESSAGE: "t4t-gateway" BEE_DATA_DIR: /home/bee/.bee # Pin homelab_bee as a static peer so the libp2p connection survives # routing-table churn. Without this, PSS chunks the homelab pushes # toward us are landing at random ff-prefix peers (e.g. ff7adaed) # instead of prod_bee at ff896a93, so the gateway never receives # provider ack/deliver envelopes. # Seed homelab_bee alongside the default mainnet bootnode so we # always dial it on boot. (We can't use --static-nodes here: Bee 2.8 # only honors it in --bootnode-mode, and bootnode-mode wrecks normal # peer discovery.) BEE_BOOTNODE: "/dns4/bee.ffaerber.duckdns.org/tcp/1634/p2p/QmZTZyeQhFBMtta9XhtHUoLE3EtDKrzv76uoQX9h9SowQC,/dnsaddr/mainnet.ethswarm.org" volumes: - bee-data:/home/bee/.bee ports: # libp2p P2P — must be reachable from the public internet so other Bee # nodes can dial back. Without this, the node only outbound-dials and # can't sustain enough peers for PSS routing. - "1634:1634" networks: - internal deploy: replicas: 1 restart_policy: condition: on-failure resources: limits: memory: 1024M reservations: memory: 512M logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push" postgres-exporter: image: quay.io/prometheuscommunity/postgres-exporter:latest networks: - internal environment: DATA_SOURCE_NAME: "postgresql://hosted:hosted@postgres:5432/hosted_gateway?sslmode=disable" deploy: resources: limits: memory: 128M reservations: memory: 64M logging: driver: loki options: loki-url: "https://loki.t4t-gateway.com/loki/api/v1/push"