grain-serve has an image, /healthz, /_status and a rollout, as knotless
ClosedThis pull adds /healthz and /_status, --check-index, a deploy/Dockerfile, and a compose project with deploy.sh, rollout.sh and logs.sh. It mirrors knotless's deploy shape.
I didn't read the whole diff. It's about 2,500 lines and the tool output was truncated, so I read the files at the head commit instead:
status.rsdeploy/docker-compose.ymldeploy/Dockerfiledeploy/rollout.sh- the
feedscall sites
I did not read deploy.sh, logs.sh, check_index or its tests, tests/main.rs, the Cargo profile or the README.
I found nothing that must change.
- Status logic: the disk thresholds and the 200/503 mapping are consistent with their tests.
- Rollout: the serve path stops the old container first when the index would migrate, and refuses to start when the index is past the build or the check can't run.
- Env var:
GRAIN_SERVE_URLis the name the web code reads. - Feeds: only Jetstream is registered as a feed, so
fedbeing true when any feed is ok matches the single-feed case. If you add a second feed, one dead feed would give 200 with a problem listed.
All four checks (test, rust, harness, check) were still queued when I looked, so none had passed. As you say, the image has never been built and deploy.sh has never run against a host. The first host build is the real test.
Since my last round the pull has one new commit: grain-serve now shuts down gracefully on SIGTERM as well as ctrl-c. That fits the rollout, because docker stops the old container with SIGTERM through tini. I read the change in lib.rs. The select! over the two signals is correct, and the handler is installed inside the shutdown future. A new test in tests/main.rs sends kill -TERM and checks that the process exits successfully. The tokio::signal::unix call is Unix-only, which is fine for a server image. Nothing from my earlier review needed a follow-up. All four checks were still queued when I looked, so none has passed yet. The image build and deploy.sh are still untested against a host, as the pull says.
Since my last round the pull has one new commit. It reformats two healthcheck.test arrays in deploy/docker-compose.yml across several lines. The commands are character-for-character the same, so nothing changes at runtime. My earlier rounds asked for nothing, so there is nothing to follow up. The image build and deploy.sh are still untested against a host, as the pull says.
Now stacked on the telemetry pull too (grain-serve logs JSON lines, a wide line a request, and slow SQL), merged in, since both changed main.rs, FLAGS and serve(); and on origin/batch/one. So it lands after flags, the batch and telemetry. The merge keeps --check-index ahead of telemetry::init, SIGTERM in serve() around telemetry::observed, and the compose file passes --metrics 127.0.0.1:9464 as knotless's does. Also: deploy/docker-compose.yml formatted as vp fmt checks (the check failure at 5b41fdd).
Since my last round the pull has merged in the batch and telemetry branches, plus one commit that adds --metrics 127.0.0.1:9464 to the serve command in deploy/docker-compose.yml. I read the compose file at the head. The flag binds to loopback inside the container, so it isn't published and can only be read with docker exec, as the comment says. The healthchecks, ports and networks are unchanged. My earlier rounds asked for nothing, so there is nothing to follow up. I did not review the merged batch and telemetry code, since those are separate pulls. All four checks (test, rust, harness, check) were still queued when I looked, so none has passed. The image build and deploy.sh are still untested against a host, as the pull says.
Landed on feature/rust at 164794a, fast-forwarded with infra/justfile (The Justfile runs grain-serve and the web, as knotless's does), which contains this head and passed every check against fc5f686. What the review skipped was read before merging: deploy.sh works only in /etc/grain-serve and /mnt/data/grain-serve under its own compose project, so it cannot touch the running /etc/grain stack; no workflow runs it; logs.sh only reads; deploy.yml now waits on more checks, not fewer.
What changes
Stacked on the flags pull (grain-serve is set by flags, as knotless-serve is), whose compose passes flags: it lands after that one.
An image of grain-serve and its pages.
deploy/Dockerfilebuilds grain-serve, thegrainCLI, grain-mcp and grain-import with--profile deploy --locked(a new[profile.deploy]in Cargo.toml: release's, without LTO across crates and in 16 units), stamped with the commit it was built from (GRAIN_COMMIT), beside the SvelteKit app built with adapter-node, under tini as the node user. Its ownDockerfile.dockerignorekeepstarget/andnode_modulesout of the context. The rootDockerfileis the old server's and is left as it is: main and hetzner-infra build it until cutover, so the two coexist by name, and at cutover this one takes the root's place./healthzand/_status, as knotless-serve answers them./healthzisokwhile it listens./_statusis 200 or 503 with JSON: Jetstream connected and bringing an event in the last two minutes (the follower now tells the index'sfeedsas it connects and hears), the index answering a read, the disk under it with room (15% low, 5% full), and the version and commit running;Cache-Control: no-store.--check-index <file>: says whether the index there is of this build's tables, without changing it, and serves nothing. grain's index migrates rather than rebuilds, so the answer is "keeps it", "would migrate it" (rollout.sh then stops the old one first) or "past this build's" (not started).deploy/, in knotless's shape:docker-compose.yml(projectgrain-serve:serveon 2600 andpageson 2601, journald logging, healthchecks withstart_interval, every setting a flag from${GRAIN_*:-}, secrets from/etc/grain-serve/serve.envon the host),deploy.sh(build / up / drop, over SSH to the host's docker, its own build cgroup, the.envwritten on the host),rollout.sh(no-gap rollover, with the index check),logs.sh(the journal through jq),local.env.example,README.md;just deployandjust logs.Why
The audit against knotless: grain-serve had no image, health or status route, index check or rollout; knotless has all of them, and grain's move off the old server deploys this way.
Mirrors knotless's
Dockerfile,[profile.deploy]inCargo.toml,/healthzand/_status(crates/knotless-serve/src/lib.rsroutes,crates/knotless-serve/src/status.rs,crates/knotless/src/status.rs),--check-index(check_indexincrates/knotless-serve/src/lib.rs), anddeploy/{docker-compose.yml,rollout.sh,deploy.sh,logs.sh,local.env.example,README.md}. Kept grain-specific: the Dockerfile isdeploy/Dockerfile, beside the old server's root one; the pages are built from the rootpackage.json, as the web still is, so the image carries its prunednode_modules(knotless's web is bundled whole); the index check speaks of migrating, not rebuilding;/_statushas no mirror probe and adds the version and commit; the feed state lives on theIndexrather thanApp, so no test or pull in flight that builds anAppchanges; deploy.sh has no runner, triager or reviewer.The hand-off to hetzner-infra
Production compose stays in
~/code/hetzner-infra(stacks/grain), which this does not change. grain-serve is a second compose project on grain's host, in/etc/grain-serve, as knotless's is in/etc/knotless, reached on an external network,grain-serve, that deploy.sh makes. At cutover hetzner-infra:grain-servenetwork, as it is onknotless, and proxies grain's domain tograin-serve:2600for grain-serve's paths andgrain-pages:2601for the rest (the Caddyfile is in deploy/README.md), or all of it tograin-serve:2600once grain-serve hands the pages on (--web, in flight elsewhere);grainservice and its build fromgrain-deploy;/etc/grain-serve/serve.envfrom its secrets (the CDN key and salt, the push key file paths, Clef's token) and moves the push key files to/mnt/data/grain-serve/certs;grain-importonce into/mnt/data/grain-serve/index.dbbefore serve first starts.deploy.yml then runs
deploy/deploy.sh buildandup, as knotless's deploy workflow does; until cutover it deploys the old server.How it was checked
Unit tests:
status.rs(feeds, thresholds, the disk, both routes and theirno-store),check_indexagainst an index at, behind and past this build's tables, andtests/main.rsrunsgrain-serve --check-index. Clippy here on grain-serve;docker compose configon the compose file;bash -n/sh -non the scripts. The checks run the rest.Shortcomings
deploy.sh buildon the host is its test.deploy.shhas not been run against a host.Review
The status module (
crates/grain-serve/src/status.rs) anddeploy/docker-compose.ymlfirst; deploy.sh and rollout.sh follow knotless's line by line.🤖 Generated with Claude Code