Files
devops/hostinger/prod-stack.yaml
T
Felix FaerberandClaude Opus 5 550f71d2ab Publish stamp-monitor at bee.t4t-gateway.com
The gateway's /bee page shows a summary of the node. This exposes the
monitor behind it, which is where batch fullness, the bucket map, wallet
and chequebook runway, and the ledger of every top-up made or refused
actually live.

Left unrouted originally on the reasoning that its UI can spend xBZZ.
That was the wrong test. The protection is that the app fails CLOSED --
every /api/admin route demands an x-admin-token and returns 503 when
ADMIN_TOKEN is unset -- not that the port was hard to reach. Relying on
the latter is how the batch went unwatched long enough to take the
gateway down; a dashboard nobody can open is a dashboard nobody reads.

Bee itself stays unpublished. Reaching it through the monitor's
passthrough still requires the admin token, which is the whole reason the
monitor sits in front of it.

Also bumps the image to pick up the fix that makes Settings caps bind
manual spending, not just the poller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YLYiaPYror1vLKApYKrxTu
2026-08-20 12:38:06 +03:00

363 lines
12 KiB
YAML

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
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:aebf7e471f41cfdf43f8b6bc8dcdba4d29bc277612a302f61b7ed805b243901c
networks: [internal, public]
volumes:
- stamp-monitor-data:/data
env_file:
- .env
environment:
BEE_URL: http://bee:1633
DB_PATH: /data/monitor.sqlite
ADMIN_TOKEN: '${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"