Files
devops/hostinger/prod-stack.yaml
T
Felix FaerberandClaude Opus 5 da48a3ac77 stamp-monitor: build stamp in the header, and no-cache on index.html
index.html was served with no cache headers at all, so a stale one could
pin the old bundle through any number of redeploys. The header now names
the build serving it.

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

376 lines
13 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
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"