Files
esh-pfi-infrastructure/stacks/althing-post-office/compose.yaml
T
vh 20af94c0a0 deploy(althing): post office to 3.6.0 — handle delete/retire verbs go live
The tools had been on 3.6.0 while the container sat on 3.0.0 for seven days, so
the operator verbs failed with "no tool named delete_handle" — they live in the
post office, not the client.

Image built on nh3-dev from a clean tree at 4d26226 and pushed under the
claude-bot namespace; the compose pin moves to the new digest rather than
floating on the tag, since this container is the fleet's whole message bus.

The backup procedure this file documents earned itself again: at stop time the
database was 23.8 MB with a 5.9 MB WAL beside it, so a plain copy would have
produced a database that opens cleanly, smokes green, and is missing the day's
mail. Stop, explicit checkpoint to a zero-byte WAL, copy, then verify counts on
both sides — 76 handles, 995 messages, 1022 recipients, integrity ok.

Post-deploy the same counts came back with handles.retired_at present, and the
memory cap and OOM guard were confirmed by `docker inspect` rather than by
reading the yaml, which is what that file asks for and the only check that can
tell a working cap from a decorative one.

Bus down about four minutes.
2026-09-05 09:40:19 -07:00

154 lines
7.6 KiB
YAML

# althing v3 — the post office. The fleet's message bus.
#
# MOVED nh3-dev -> nh3-docker on 2026-08-28 (operator: "I want it on the docker
# machine — that was always the goal"). It was deployed to nh3-dev at the U9b flag
# day because the herald lives there; but the herald is the piece that MUST be
# host-local (it reads route files and pokes FIFOs), and the post office is
# explicitly the piece that is not.
#
# Three reasons the dev box was wrong:
# - our own server table calls nh3-dev "not a Docker-stack host", and NH-site
# non-GPU services belong here.
# - nh3-dev had three OOM events in fourteen days with the interval HALVING
# (2026-08-14, -26, -28), and the confirmed hog is Claude Code sessions at
# 5-18 GB each, which is that box's actual job.
# - `mem_limit` protects the fleet FROM the post office. It does nothing to
# protect the post office from the box: oom_score_adj is 0, so it was an
# ordinary kill candidate, and the 08-28 sweep took althing-herald and
# uvicorn. A sweep that takes the post office takes mail for all 73 handles.
#
# ⚠ MOVING THE DATA: `docker stop` does NOT checkpoint the WAL. Measured on the
# 08-28 move: post_office.db was 155 KB / mtime 15:09 while post_office.db-wal
# was 4.1 MB / mtime 16:56 — every recent message lived in the WAL. Copying the
# .db alone yields a database that opens cleanly, passes a smoke test, and is
# missing the day's mail. Stop the container, then
# `PRAGMA wal_checkpoint(TRUNCATE)` explicitly, then verify row counts on BOTH
# sides before deleting anything.
#
# Deploy: scripts/deploy-stack.sh nh3-docker althing-post-office
# Image: built from the althing repo's Dockerfile (vh/althing @ v3.0.0), pushed to
# the gitea registry 2026-08-28. Rebuild + republish:
#
# cd ~/development/althing
# docker build -t gitea.phasefinal.com/claude-bot/althing-post-office:<ver> .
# echo $(cat ~/.config/claude-bot/gitea-token) | \
# docker login gitea.phasefinal.com -u claude-bot --password-stdin
# docker push gitea.phasefinal.com/claude-bot/althing-post-office:<ver>
#
# then update the digest below and redeploy. Supersedes the
# `docker save | ssh | docker load` hand-carry the move originally used.
#
# ⚠ NAMESPACE IS `claude-bot`, NOT `vh`. claude-bot's token carries write:package
# but package namespaces are owned: pushing to `vh/...` returns
# `unauthorized: authentication required` AFTER a successful `docker login`, which
# reads like a credential fault and is actually an ownership one. Publishing under
# claude-bot's own namespace also satisfies the standing directive to stop reusing
# the operator's personal credentials for infra work. Both hosts are logged in as
# claude-bot; a new host needs that login before it can pull.
services:
post-office:
# Digest-pinned, not tag-floating: `:3.0.0` is a mutable pointer on a registry
# anyone can re-push, and this container is the fleet's whole message bus. The
# tag is kept alongside the digest purely so a human can read what it is.
image: gitea.phasefinal.com/claude-bot/althing-post-office:3.6.0@sha256:13158835488a8ec04f990c97c4f4c68f1d923b12494319cf07392552e68f8a78
container_name: althing-post-office
# ─── Host networking, so the bind guard keeps working ────────────
#
# ⚠ DO NOT "fix" this into a bridged container with `-p`. api.py's
# resolve_bind_host refuses any address that resolves to a wildcard, and it
# tests the RESOLVED PROPERTY rather than matching strings, so there is no
# spelling of "everything" that gets past it. A bridged container cannot
# satisfy that guard honestly: inside its own netns the only reachable bind
# is a wildcard, and publishing the port would move access control from the
# address the application checks to a `-p` flag it cannot see.
#
# Reachability on the private network IS the authorisation story here —
# there is no login and none is wanted.
#
# Cost, stated plainly: no network namespace isolation, and port 8390 is
# claimed host-wide. For a single-service private-network deployment that is
# the right trade, but it IS a trade.
network_mode: host
# The store is the only thing that must survive. Named volume rather than a
# bind mount: uid 1000 inside the container owns it, and docker creates it
# with the right ownership instead of inheriting the host path's.
volumes:
- post-office-data:/var/lib/althing
# The private address of THIS box. Single place it is named; the image ships
# no default on purpose, so a deployment that omits it is refused at startup
# rather than binding wide.
environment:
ALTHING_BIND_HOST: "10.100.50.40"
ALTHING_PORT: "8390"
ALTHING_DB: "/var/lib/althing/post_office.db"
# `unless-stopped` rather than `always` so an operator who deliberately stops
# it during a flag day does not find it running again after a reboot.
restart: unless-stopped
# One Python interpreter holding one SQLite connection; it idles far below
# this. The cap is not a tuning parameter, it is a promise that the post
# office can never be its host's next OOM story.
#
# ⚠ nh3-docker runs Compose v5, which ignores `version:` and honours
# `mem_limit` directly. On a docker-compose 1.x host this needs schema 2.4 —
# under 3.x the key moves to `deploy:`, which is swarm-only and SILENTLY
# IGNORED. Verify with `docker inspect` (want 536870912), never by reading
# the yaml: a cap that does nothing reads as protection.
mem_limit: 512m
# ⚠ The fleet's entire bus. Make the kernel shoot almost anything else first.
# This is the gap the nh3-dev deployment had: a 512m cap and oom_score_adj 0
# means "cannot cause an OOM, is an ordinary victim of one".
oom_score_adj: -500
# Docker's default json-file driver has no size limit. v2's herald left a
# 60 MB log on nh3-dev; an uncapped container log is the same mistake with a
# different name.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# Inherited from the image, restated so it is visible at deploy time rather
# than only in `docker inspect`.
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://10.100.50.40:8390/',timeout=4).status==200 else 1)"
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# `Toolchain` is an EXISTING group under the existing Toolchain tab in
# stacks/homepage/conf/settings.yaml — "the plumbing", which is where a
# message bus belongs. Naming a group the layout has never heard of gets no
# `tab:` and renders the group on ALL FOUR tabs (the Scriberr "AI Systems"
# bug, 2026-08-23), so this must stay a group that already exists.
#
# ⚠ Labels bind at container CREATION. Editing them needs `up -d`, never
# `restart` — a restart leaves the old labels in place and the dashboard
# keeps showing whatever was there before.
#
# nh3-docker is a discovered Docker host in homepage's docker.yaml (as
# `nh3-pfi-docker`), so the label is enough; do NOT also add a services.yaml
# entry or the card renders twice.
labels:
- homepage.group=Toolchain
- homepage.name=althing post office
- homepage.icon=mdi-mailbox
- homepage.description=althing v3 message bus — handles, unread counts, node liveness
- homepage.href=http://10.100.50.40:8390
volumes:
post-office-data:
name: althing-post-office-data