Files
devops/hostinger/prod-stack.yaml
T
Felix FaerberandClaude Opus 5 b4c62905e6 Add stamp-monitor; move Bee to 2.8.1
This is the config that was already running on the host, brought back
into the repo where it belongs, plus the Bee bump it implies.

stamp-monitor watches prod_bee's postage batches and renews them before
they lapse. It exists because nothing did: the gateway's batch fell under
minTtlDays, the gateway tried to renew it itself, the top-up (64.64 xBZZ
on a depth-24 batch) exceeded the wallet, and the throw took down every
route that boots the gateway context. A batch running out was never
supposed to be able to return 500 on /v1/models.

It runs next to Bee rather than reading the chain from elsewhere because
bucket fullness is not on-chain -- stamps are off-chain signatures, so
only a node that saw them knows how full a batch is. A batch can be fully
funded and still refuse writes, and that is the failure the chain cannot
show you.

Bee 2.8.0 -> 2.8.1 for a related reason: 2.8.0 omits utilizationRatio
from /stamps, so a batch at 215 of 256 reported as empty. An absent
number read as zero, and zero means "plenty of room" -- fullness alerts
would have stayed quiet on exactly the batch that needed them. The
monitor now derives the figure either way, but the node should not be the
thing lying. Digest-pinned: this node holds a chequebook and a stake.

BEE_STAMP_MANAGE defaults to false in the example now, and the batch ID
is pinned. Leaving the managed path reachable is what caused the outage,
and an empty BEE_POSTAGE_BATCH_ID is what re-enables it.

The dashboard is deliberately not routed through Traefik -- it can spend
xBZZ. Readme documents the tunnel instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YLYiaPYror1vLKApYKrxTu
2026-08-19 01:57:18 +03:00

344 lines
11 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:62cf97f72090879f8d470fc9f2a09ab60d1d3a29dd658dfe2cd3a313c2b23540
networks: [internal]
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
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"