Move migrations out of the api container into a migrate service

The dev stack ran schema work from the api entrypoint while the pull and
airgap stacks ran it from a dedicated migrate service. Same repo, two
different init paths, and the dev one was missing a step.

That gap was real, not theoretical: the first run of this service on a
stack that had been up for days created 13 default settings. The entrypoint
only ran `db upgrade` and `plugin upgrade-all`, never the seeds, so any
patch adding a permission or a setting reached pull/airgap sites and
silently skipped dev. endpoints.agentupdate was exactly that shape of
change.

Running init from the api entrypoint has two other problems. Under
`restart: unless-stopped` a failing migration crash-loops, re-attempting
partially-applied schema work and burying the original error under retry
noise; the migrate service uses `restart: "no"` so a failure exits once and
stays readable. And `plugin upgrade-all` was tolerated as a warning, so the
app would start with plugin tables missing - the shape of the
endpointlogcollections bug. Chained with && it now stops the rollout
instead.

No fallback path is kept in the entrypoint. Compose honours
service_completed_successfully even when a single service is named, so
`up -d api` still runs migrate to completion first - verified, api stays in
Created until migrate exits 0. A skip-flag would have meant the safe
behaviour depended on remembering to set a variable, and deployment here
goes through an install script that invokes compose, so there is no second
caller to protect. The entrypoint keeps only the database wait, which is
about this container's readiness to serve rather than schema state.

The seeds are all create-if-missing, so running them on every start is a
no-op once current: the second run reported 0 permissions, 0 settings and
scopes already up to date. Ordering in the chain is load-bearing - plugin
tables cannot be created before the plugins are installed, and seedscopes
only links permissions that already exist as rows, so seeds run last.

migrate carries the full app environment including CORS_ORIGINS even though
it never serves a request, because create_app() validates config before any
CLI command dispatches and the production config refuses to build without an
explicit allowlist. It mounts instance_data because bootstrap-plugins.sh
writes plugins.json there; it and api never run concurrently, so the shared
mount is not contended.

The Dockerfile's standalone `docker run` note now states that the image does
not migrate itself and lists the sequence to run first.
This commit is contained in:
benjamin.baker
2026-09-10 15:50:36 -04:00
parent 8be1c2379f
commit 58a3670529
3 changed files with 81 additions and 21 deletions

View File

@@ -17,6 +17,13 @@
# docker build -t shopdb-flask .
# Run (with .env):
# docker run --env-file .env -p 5001:5001 shopdb-flask
#
# Note: this image does not migrate its own database on start. A plain
# `docker run` expects the schema to already be current; run the same sequence
# the compose `migrate` service does first:
# flask db upgrade && ./scripts/bootstrap-plugins.sh && flask plugin upgrade-all
# && flask seed permissions && flask seed settings && flask seed reference-data
# The supported path is `docker compose up`, which runs that for you.
# ---- Stage 1: build the Vue frontend ----
FROM node:20-slim AS frontendbuild
@@ -142,7 +149,10 @@ USER shopdb
EXPOSE 5001
# Entrypoint runs migrations and bootstraps plugins on first deployment
# Entrypoint waits for the database, then execs the CMD. Migrations, plugin
# bootstrap and seeds are NOT run here - they belong to the `migrate` service
# in the compose files so they happen exactly once per deployment rather than
# on every container restart.
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
# Probe the public health route, not the login endpoint. Probing POST