#!/bin/sh # Apply pending Prisma migrations, then hand off to the API. # # WHY THE CONTAINER AND NOT THE DEPLOY WORKFLOW # # The workflow still has its own `prisma migrate deploy` step and that is not # redundant: it runs BEFORE the new images are pulled, i.e. while the OLD code # is still serving, which is the order expand/contract migrations are designed # around (see docs/DEPLOY_AND_MIGRATIONS.md). Doing it here as well closes the # gaps that step cannot: # # - The runner has to reach MySQL directly. When it cannot, the deploy is run # with `skip_migrate=true` and the schema silently does not move — the app # then boots against a schema that is one release behind, which surfaces # later as a column-not-found at runtime rather than as a failed deploy. # - A container restarted by `restart: unless-stopped` after a host reboot, # or a stack re-applied by hand in Portainer, never goes through the # workflow at all. # # `migrate deploy` is idempotent, so running it in both places costs one # no-op query on the normal path. # # THE API DOES NOT START IF THE MIGRATION FAILS. That is deliberate: serving # against a schema that does not match the code is worse than being down, # because the failures it produces are partial and silent (a write to a column # that does not exist yet fails for one feature while the rest of the app looks # healthy). The container exits non-zero and Docker's restart policy retries. set -e SCHEMA=/repo/packages/database/prisma/schema.prisma log() { echo "[entrypoint] $*"; } if [ "${RUN_MIGRATIONS:-true}" != "true" ]; then log "RUN_MIGRATIONS=${RUN_MIGRATIONS} — skipping migrations, starting the API" exec "$@" fi if [ -z "${DATABASE_URL}" ]; then log "DATABASE_URL is unset; cannot migrate." >&2 log "Set it, or set RUN_MIGRATIONS=false if you migrate out of band." >&2 exit 1 fi # pnpm's hoisted linker normally puts the CLI in the root .bin, but the # workspace package keeps its own link too. Accept either rather than pinning # a layout detail of the installer — the Dockerfile asserts at build time that # one of these exists, so a missing CLI breaks the image build, not a deploy. PRISMA="" for candidate in /repo/node_modules/.bin/prisma \ /repo/packages/database/node_modules/.bin/prisma; do if [ -x "$candidate" ]; then PRISMA="$candidate" break fi done if [ -z "$PRISMA" ]; then log "prisma CLI not found in this image; cannot migrate." >&2 exit 1 fi # Retry ONLY a connection failure (P1001). On a full bring-up the database # container can still be starting, and on galactus the API additionally has to # resolve the host's MagicDNS name — a lookup that is unreliable for the first # moments after a host reboot (see the dns block in the app compose file, and # docs/DEPLOY_AND_MIGRATIONS.md). # # Every other failure exits immediately. Retrying a migration that is actually # broken just delays the same error behind a minute of noise, and P3005 in # particular needs a human. attempt=1 max="${MIGRATE_MAX_ATTEMPTS:-20}" delay="${MIGRATE_RETRY_SECONDS:-3}" while : ; do log "prisma migrate deploy (attempt ${attempt}/${max})" if output=$("$PRISMA" migrate deploy --schema "$SCHEMA" 2>&1); then printf '%s\n' "$output" log "migrations up to date" break fi printf '%s\n' "$output" >&2 if ! printf '%s' "$output" | grep -q 'P1001'; then log "migrate deploy FAILED — refusing to start the API." >&2 if printf '%s' "$output" | grep -q 'P3005'; then log "P3005: the database has tables but no migration history. This is a" >&2 log "database that predates Prisma migrations. Baseline it ONCE with:" >&2 log " npx prisma@5 migrate resolve --applied 0000_init --schema $SCHEMA" >&2 fi exit 1 fi if [ "$attempt" -ge "$max" ]; then log "database unreachable after ${max} attempts — giving up." >&2 exit 1 fi attempt=$((attempt + 1)) sleep "$delay" done exec "$@"