Skip to content

Stop a git checkout being able to change what nginx serves - #301

Merged
adamjohnwright merged 3 commits into
mainfrom
fix/nginx-staged-config
Sep 25, 2026
Merged

adamjohnwright merged 3 commits into
mainfrom
fix/nginx-staged-config

Conversation

@adamjohnwright

@adamjohnwright adamjohnwright commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

What shipped is the baked image, not the staging directory. The first commit on this branch used a gitignored .staged/ directory and scripts/stage-nginx.sh; it was replaced on review with the standard approach below. The squash commit message on main (dea62c1) still describes the staging version — this description is the accurate record.

nginx mounted deploy/nginx straight out of the working tree, which made a working directory a deployment input: check out a branch and the routes inside the running container were that branch's, one reload away from live. A feature branch's /mcp route was found sitting in the production-serving container this way, inert only because nginx reads its configuration at start.

What shipped

deploy/nginx/Dockerfile bakes the configuration into an image, which is what the other three services in this compose file (render, content-node, mcp) already do. The running config is a property of an image, not of whatever is checked out.

Certificates stay as runtime mounts — they're host secrets, and nothing that can't be rebuilt from this repository belongs in the image.

NGINX_ENV is a build argument defaulting to dev, exactly as the old mount did, so behaviour on the indexed host is unchanged. It still matters: building with the dev config on the production host would deindex reactome.org.

Deploying a config change

docker compose build nginx
docker compose run --rm nginx nginx -t     # validate, with the cert mounts
docker compose up -d nginx

Not a build-time RUN nginx -t: the config references certificates mounted at run time, so it would fail on every host for the wrong reason.

Side effect worth knowing

The old single-file bind mount followed the inode, and git checkout replaces files rather than rewriting them — so the container's environment config had been four days stale while the directory beside it was current. That's what made a /mcp reload fail with host not found in upstream "mcp". Baking removes the mount entirely.

Verified

Built, validated with nginx -t, deployed; site 200, /mcp initialises, the Java exporter and /RenderService render, and the container has no configuration mounts left.

Also caught a live regression that wasn't this PR's: /ContentService/data/species/main answered 404 because #289 added its nginx route while content-node still ran the image from before those endpoints existed. Rebuilt and redeployed content-node; fixed.

🤖 Generated with Claude Code

adamjohnwright and others added 3 commits September 22, 2026 12:57
nginx mounted `deploy/nginx` straight out of the working tree, which makes a
working directory a deployment input: check out a branch and the routes inside
the running container are that branch's, one reload away from live.

Found the hard way. A feature branch's `/mcp` route was sitting inside the
production-serving container while that branch was checked out here -- inert
only because nginx reads its configuration at start rather than continuously,
so any reload for any unrelated reason would have deployed it. I noticed,
returned the tree to main, and left the mechanism in place; this removes it.

The container now mounts `.staged/`, which is gitignored and so untouched by
any checkout, and `scripts/stage-nginx.sh` is the only thing that writes there.
It runs `nginx -t` against what it is about to stage, in the same image, so an
invalid configuration never reaches the path a reload reads -- verified by
appending a bad directive and watching it refuse.

Copying **in place** rather than replacing is load-bearing and fixes a second
fault at the same time. A single-file bind mount follows the inode and `git
checkout` replaces a file rather than rewriting it, which is why this
container's environment config was four days stale while the directory beside
it was current -- the reason a `/mcp` reload failed earlier with "host not
found in upstream". `cp` truncates and writes, so the inode survives and a
reload is now sufficient where a recreate used to be required.

Proven both ways against the running container: an edit in the working tree is
invisible to it, staging makes that same edit visible without recreating, and
restoring the file cleans it. `nginx -t` passes inside the container and the
site answers 200 throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the staging directory from the previous commit with the standard
answer, which is also the one this compose file already uses three times over:
`render`, `content-node` and `mcp` all bake their code into an image. nginx was
the odd one out, reading its configuration from whatever was checked out.

What that cost: a feature branch's /mcp route was found sitting inside the
production-serving container, inert only because nginx reads its configuration
at start rather than continuously. The staged-directory fix worked, and was
something a newcomer had to discover. An image is a thing they already
understand, and "which configuration is live" becomes readable off the image
rather than inferred from a working tree.

Gone with it: scripts/stage-nginx.sh, the .staged directory and its gitignore
entry. The validation it did is now a documented step against the built image,
which exercises the certificate mounts that a build-time `nginx -t` cannot see:

    docker compose build nginx
    docker compose run --rm nginx nginx -t
    docker compose up -d nginx

Certificates stay as runtime mounts. They are host secrets and nothing that
cannot be rebuilt from this repository belongs in the image.

Verified after deploying: the site answers 200, `/mcp` initialises, the Java
exporter renders, `/RenderService` renders, and the container has no
configuration mounts left at all.

**And it caught a live regression that was not mine.** `/ContentService/data/
species/main` was answering 404 on beta: #289 added the nginx route to node and
merged, but content-node was still running the image built before it -- ten
endpoints where main has twelve -- so a path Java used to answer had been
turned into a 404. Rebuilt and redeployed; all four node-served endpoints now
answer 200 and species/main leads with Homo sapiens.

That is the third time today a merge sat undeployed. The nginx half now cannot
drift, because the image is built from the repository; the services behind it
still can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit dea62c1 into main Sep 25, 2026
6 checks passed
@adamjohnwright
adamjohnwright deleted the fix/nginx-staged-config branch September 25, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant