They were rendering inside the Policy card's title band. Also fixes 'Up to NaN undefined' shown to read-only visitors on the Files card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLYiaPYror1vLKApYKrxTu
Hostinger (production) stacks
Docker Swarm stack files for the t4t hosted-gateway, deployed to a single
Hostinger VPS at ssh root@195.35.25.26 and reachable at
t4t-gateway.com.
Layout mirrors cipherdolls/devops/hostinger/.
Stacks
| Stack | File | What it runs |
|---|---|---|
system |
system-stack.yaml |
Traefik (TLS + ingress), Grafana, Loki, Prometheus, node-exporter, cadvisor. Owns the public + internal overlay networks. |
prod |
prod-stack.yaml |
hosted-gateway api + worker + one-shot prisma migrate, plus postgres / redis / bee / postgres-exporter / stamp-monitor. |
dev |
dev-stack.yaml |
Staging on dev.t4t-gateway.com — the :develop image tag against its own postgres / redis / bee volumes. |
Supporting configs:
loki.yaml— Loki single-binary filesystem configprometheus.yml— scrape targets (traefik, grafana, loki, node-exporter, postgres-exporter, cadvisor, hosted-gateway api).env.example— copy to.envon the host, fill inOPERATOR_PRIVATE_KEY/JWT_SECRET_KEY/ etc.
DNS records (Hostinger DNS panel)
Point all of these at 195.35.25.26:
A t4t-gateway.com 195.35.25.26
A www.t4t-gateway.com 195.35.25.26
A api.t4t-gateway.com 195.35.25.26
A bee.t4t-gateway.com 195.35.25.26
A dev.t4t-gateway.com 195.35.25.26
A traefik.t4t-gateway.com 195.35.25.26
A grafana.t4t-gateway.com 195.35.25.26
A loki.t4t-gateway.com 195.35.25.26
A prometheus.t4t-gateway.com 195.35.25.26
One-time host setup
ssh root@195.35.25.26
# Docker + swarm
curl -fsSL https://get.docker.com | sh
docker swarm init --advertise-addr 195.35.25.26
# Loki driver (so container logs ship to Loki via the `loki` logging driver)
docker plugin install grafana/loki-docker-driver:2.9.7 \
--alias loki --grant-all-permissions
# Stack files come from this repo, cloned on the host. Deploys run from the
# clone, so `git pull` is the deploy step — do not hand-edit files here.
git clone git@github.com:token-for-token/devops.git /root/devops
cd /root/devops/hostinger
cp .env.example .env && chmod 600 .env
$EDITOR .env # fill in OPERATOR_PRIVATE_KEY, JWT_SECRET_KEY, ...
Deploy
The system stack creates the shared overlay networks, so deploy it first.
cd /root/devops/hostinger
# 1. Networks + ingress + monitoring
docker stack deploy -c system-stack.yaml system
# 2. App + datastores
docker stack deploy -c prod-stack.yaml prod
# (optional) staging on dev.t4t-gateway.com
docker stack deploy -c dev-stack.yaml dev
Re-deploying after a new image tag:
docker service update --image ghcr.io/token-for-token/hosted-gateway:main \
--force prod_api
docker service update --image ghcr.io/token-for-token/hosted-gateway:main \
--force prod_worker
Secrets
OPERATOR_PRIVATE_KEY is the on-chain identity for every tenant. For now it
lives in /root/devops/hostinger/.env (chmod 600). Future work: migrate to a
proper Docker secret backed by the host's secret manager — never an env var
in the stack file.
Postage batches (stamp-monitor)
The gateway's uploads are paid for by a postage batch that expires. When it
lapsed, the gateway returned 500 on every route — see the comment above the
stamp-monitor service for the full chain. stamp-monitor now owns renewal:
it polls prod_bee every 5 min, tops a batch up when it drops under 14 days,
and refuses any single action over 8 xBZZ.
It runs beside Bee rather than reading the chain because bucket fullness is not on-chain — postage 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.
Published at bee.t4t-gateway.com. The
gateway's own /bee page shows a summary; this is the full view — batch
fullness and the bucket map, wallet and chequebook runway, and the ledger of
every top-up it has made or refused.
It is publishable because the app fails closed: every /api/admin route
requires an x-admin-token and returns 503 if ADMIN_TOKEN is unset, so a
misconfigured deploy is a visible outage rather than an open door onto
endpoints that spend xBZZ. The Bee node stays unpublished; reaching it through
the monitor's passthrough still requires that token.
From the shell, note internal is not attachable, so docker run --network internal ... is refused — query through a container already on it:
docker service logs --tail 50 prod_stamp-monitor
M=$(docker ps --filter name=prod_stamp-monitor -q | head -1)
docker exec $M bun -e 'console.log(await (await fetch("http://bee:1633/stamps")).text())'
The API authenticates with an x-admin-token header, not Authorization: Bearer. A wrong header is a 401; a missing ADMIN_TOKEN in the service is a
503 (admin API disabled) — it fails closed, because these routes buy postage.
Deploy caveat: docker stack deploy substitutes ${STAMP_MONITOR_ADMIN_TOKEN}
from the shell, not from .env. Source it first or the monitor deploys with an
empty token and the dashboard 503s:
cd /root/devops/hostinger && set -a && . ./.env && set +a
docker stack deploy -c prod-stack.yaml --resolve-image=never prod
WEBHOOK_URL is unset, so alerts (wallet_low, batch_full, topup_blocked)
only reach the logs. Set it to make them reach a person.
Backups
Postgres volume:
docker run --rm \
-v prod_postgres-data:/from_volume \
-v $(pwd):/workdir \
alpine \
sh -c "cd /from_volume && tar cvf /workdir/prod_postgres-data.tar ."
scp root@195.35.25.26:/root/devops/hostinger/prod_postgres-data.tar ~/Downloads
Bee data volume (postage batch / swarm keys — DO NOT lose this):
docker run --rm \
-v prod_bee-data:/from_volume \
-v $(pwd):/workdir \
alpine \
sh -c "cd /from_volume && tar cvf /workdir/prod_bee-data.tar ."
Troubleshooting
docker stack services system # health of ingress + monitoring
docker stack services prod # health of gateway + data
docker service logs -f prod_api
docker service logs -f prod_worker
# Replay a failed Prisma migration:
docker exec -it $(docker ps -q -f name=prod_prisma) sh
bunx prisma migrate resolve --rolled-back <migration_name>