From 7f640649b98a4145a38f00944bd37ee7e21bfc68 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sun, 7 Jun 2026 10:50:28 -0400 Subject: [PATCH] Auto-apply migrations on deploy (entrypoint + one-shot service) So a deploy never needs a manual `alembic upgrade head`: - Backend image gains an entrypoint that runs `alembic upgrade head` before uvicorn when RUN_MIGRATIONS=1 (set on the backend service). This self-migrates even on a Watchtower in-place image swap, which doesn't re-run one-shot jobs. - A one-shot `migrate` service covers the `docker compose up` path; backend and worker depend on it completing, which also serializes it with the backend entrypoint so alembic never runs concurrently. `upgrade head` is idempotent. Activating this needs the updated compose on the host once (Watchtower only swaps images, not the compose file / env). After that, migrations are automatic. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/Dockerfile | 4 ++++ backend/docker-entrypoint.sh | 14 ++++++++++++++ deploy/docker-compose.yml | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 backend/docker-entrypoint.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index 0340f0f..7b9b09e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -21,7 +21,11 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY app ./app COPY alembic.ini ./alembic.ini COPY migrations ./migrations +COPY docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh EXPOSE 8000 +# The entrypoint runs migrations first when RUN_MIGRATIONS=1, then the command. +ENTRYPOINT ["./docker-entrypoint.sh"] CMD ["uv", "run", "--no-dev", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100644 index 0000000..ff4a306 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Container entrypoint. When RUN_MIGRATIONS=1 (set on the backend service), +# apply DB migrations before handing off to the command. This makes a deploy +# self-migrating even when images are swapped in place (e.g. by Watchtower), +# without a separate orchestration step. `alembic upgrade head` is idempotent — +# a no-op when the schema is already current. +set -e + +if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then + echo "[entrypoint] applying database migrations (alembic upgrade head)…" + uv run --no-dev alembic upgrade head +fi + +exec "$@" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3fdbd75..4e81f73 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -40,12 +40,36 @@ services: retries: 10 restart: unless-stopped + # One-shot schema migration: runs `alembic upgrade head` and exits. Backend + # and worker wait for it to finish, so on `docker compose up` the schema is + # always current before the app serves traffic — no manual migrate step. + # NOTE: a pure Watchtower image-swap recreates only the long-running + # containers, not this one-shot job, so Watchtower deploys should be paired + # with a `compose up` (see deploy docs) to re-run migrations. + migrate: + image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main} + command: ["uv", "run", "--no-dev", "alembic", "upgrade", "head"] + labels: + com.centurylinklabs.watchtower.enable: "true" + environment: + APP_ENV: ${APP_ENV:-development} + DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance} + depends_on: + postgres: + condition: service_healthy + restart: "no" + backend: image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main} labels: com.centurylinklabs.watchtower.enable: "true" environment: APP_ENV: ${APP_ENV:-development} + # Self-migrate on start so a Watchtower in-place image swap applies any new + # migrations (idempotent). The one-shot `migrate` service covers the same + # for `compose up`; the depends_on below serializes them so they never run + # alembic concurrently. + RUN_MIGRATIONS: "1" DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance} S3_ENDPOINT_URL: ${S3_ENDPOINT_URL:-http://minio:9000} S3_BUCKET: ${S3_BUCKET:-provenance} @@ -57,6 +81,8 @@ services: condition: service_healthy minio: condition: service_healthy + migrate: + condition: service_completed_successfully healthcheck: test: - CMD-SHELL @@ -89,6 +115,8 @@ services: condition: service_healthy minio: condition: service_healthy + migrate: + condition: service_completed_successfully restart: unless-stopped frontend: