fix(supersync): harden deploy database migration

This commit is contained in:
Johannes Millan 2026-05-12 23:24:13 +02:00
parent 0c93e8492c
commit 88961485fd
5 changed files with 80 additions and 16 deletions

View file

@ -51,6 +51,11 @@ are disabled by default so app restarts cannot race the deploy migrator.
`./scripts/deploy.sh` runs `prisma migrate deploy` once before replacing the app
container, then brings the stack up and verifies the health endpoint.
Leave `DATABASE_URL` unset when using the bundled Postgres service. The default
connection uses `postgres:5432`; existing installs that already set
`DATABASE_URL` with `db:5432` keep working because the Compose service exposes
`db` as a network alias.
> **Upgrade note:** because `RUN_MIGRATIONS_ON_STARTUP` defaults to `false`,
> `docker compose pull && docker compose up -d` can leave the app running
> against unapplied migrations. Use `./scripts/deploy.sh` for production

View file

@ -79,10 +79,10 @@ services:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test:
[
'CMD-SHELL',
'pg_isready -U ${POSTGRES_USER:-supersync} -d ${POSTGRES_DB:-supersync}',
]
- CMD-SHELL
- >
pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" &&
psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -c "SELECT 1" > /dev/null 2>&1
interval: 10s
timeout: 5s
retries: 5
@ -91,7 +91,10 @@ services:
- no-new-privileges:true
mem_limit: 1g
networks:
- internal
internal:
aliases:
# Keep existing .env files with DATABASE_URL=...@db:5432/... working.
- db
logging: *default-logging
caddy:

View file

@ -28,7 +28,9 @@ POSTGRES_PASSWORD=your-secure-postgres-password
# POSTGRES_DB=supersync
# External PostgreSQL connection string. Leave unset to use the bundled compose
# postgres service. If set, also set POSTGRES_SERVICE= below.
# postgres service. The bundled service is reachable as postgres:5432; db:5432
# is also kept as a compatibility alias for older .env files. If you point this
# at an external host, also set POSTGRES_SERVICE= below.
# DATABASE_URL=postgresql://supersync:password@example.com:5432/supersync
# =============================================================================

View file

@ -59,13 +59,31 @@ echo "==> Pulling latest code..."
git pull --ff-only || { echo "WARNING: git pull failed — continuing with current files"; }
echo ""
# Load GHCR credentials from .env (for private images)
if [ -f ".env" ]; then
GHCR_VARS=$(grep -E '^(GHCR_USER|GHCR_TOKEN)=' ".env" 2>/dev/null | xargs || true)
if [ -n "$GHCR_VARS" ]; then
export $GHCR_VARS
# Load deploy-script settings from .env when they were not already exported.
load_env_value() {
local key="$1"
local line
if [ -n "${!key+x}" ] || [ ! -f ".env" ]; then
return
fi
fi
line=$(grep -E "^${key}=" ".env" 2>/dev/null | tail -n 1 || true)
if [ -z "$line" ]; then
return
fi
local value="${line#*=}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
export "$key=$value"
}
for env_key in GHCR_USER GHCR_TOKEN DATABASE_URL POSTGRES_SERVICE POSTGRES_WAIT_TIMEOUT MIGRATION_TIMEOUT DEPLOY_WAIT_TIMEOUT; do
load_env_value "$env_key"
done
# Login to GHCR if credentials provided
if [ -n "${GHCR_TOKEN:-}" ] && [ -n "${GHCR_USER:-}" ]; then
@ -111,7 +129,11 @@ fi
# running app available while online index builds run, and it fails the deploy
# before the app is restarted if Prisma cannot apply a migration.
POSTGRES_WAIT_TIMEOUT="${POSTGRES_WAIT_TIMEOUT:-60}"
POSTGRES_SERVICE="${POSTGRES_SERVICE:-postgres}"
POSTGRES_SERVICE="${POSTGRES_SERVICE-postgres}"
if [ "$POSTGRES_SERVICE" = "postgres" ] && [[ "${DATABASE_URL:-}" == *@db:5432/* ]]; then
export DATABASE_URL="${DATABASE_URL/@db:5432/@postgres:5432}"
echo "==> Rewriting legacy bundled DATABASE_URL host db to postgres for this deploy"
fi
echo ""
if [ -n "$POSTGRES_SERVICE" ]; then
echo "==> Ensuring $POSTGRES_SERVICE is running (wait timeout: ${POSTGRES_WAIT_TIMEOUT}s)..."
@ -125,10 +147,31 @@ echo ""
# for arbitrarily long. Wrap the migrator with a timeout so a stuck deploy fails
# loudly instead of hanging this script forever. Exit code 124 = timed out.
MIGRATION_TIMEOUT="${MIGRATION_TIMEOUT:-900}"
MIGRATOR_RUN="docker compose $COMPOSE_FILES run --rm --no-deps --interactive=false -T supersync"
echo "==> Verifying database connectivity from the supersync image..."
set +e
timeout "$POSTGRES_WAIT_TIMEOUT" \
$MIGRATOR_RUN sh -ec 'printf "SELECT 1;" | npx prisma db execute --schema prisma/schema.prisma --stdin > /dev/null'
DB_CHECK_STATUS=$?
set -e
if [ "$DB_CHECK_STATUS" -eq 124 ]; then
echo ""
echo "ERROR: database connectivity check timed out after ${POSTGRES_WAIT_TIMEOUT}s."
echo " Check DATABASE_URL and the compose Postgres service health."
exit 1
fi
if [ "$DB_CHECK_STATUS" -ne 0 ]; then
echo ""
echo "ERROR: database connectivity check failed (exit $DB_CHECK_STATUS)."
echo " Check DATABASE_URL. For the bundled database, leave it unset or use postgres:5432."
exit "$DB_CHECK_STATUS"
fi
echo " Database reachable"
echo ""
echo "==> Applying database migrations before app restart (timeout: ${MIGRATION_TIMEOUT}s)..."
set +e
timeout "$MIGRATION_TIMEOUT" \
docker compose $COMPOSE_FILES run --rm --no-deps supersync npx prisma migrate deploy
$MIGRATOR_RUN sh -ec 'echo " Migrator container started"; npx prisma migrate deploy'
MIGRATE_STATUS=$?
set -e
if [ "$MIGRATE_STATUS" -eq 124 ]; then

View file

@ -61,14 +61,20 @@ describe('performance migrations', () => {
const deployScript = readFileSync(join(currentDir, '../scripts/deploy.sh'), 'utf8');
const dockerfile = readFileSync(join(currentDir, '../Dockerfile'), 'utf8');
const composeFile = readFileSync(join(currentDir, '../docker-compose.yml'), 'utf8');
const migrationCommand = 'run --rm --no-deps supersync npx prisma migrate deploy';
const migrationCommand = 'npx prisma migrate deploy';
const startCommand = 'up -d --wait --wait-timeout "$WAIT_TIMEOUT"';
const externalDbStartCommand =
'up -d --wait --wait-timeout "$WAIT_TIMEOUT" --no-deps supersync caddy';
expect(deployScript).toContain('POSTGRES_WAIT_TIMEOUT');
expect(deployScript).toContain('POSTGRES_SERVICE="${POSTGRES_SERVICE:-postgres}"');
expect(deployScript).toContain('load_env_value()');
expect(deployScript).toContain('POSTGRES_SERVICE="${POSTGRES_SERVICE-postgres}"');
expect(deployScript).toContain('@db:5432');
expect(deployScript).toContain('@postgres:5432');
expect(deployScript).toContain('run --rm --no-deps --interactive=false -T supersync');
expect(deployScript).toContain('prisma db execute');
expect(deployScript).toContain(migrationCommand);
expect(deployScript).toContain('Migrator container started');
expect(deployScript).toContain(externalDbStartCommand);
expect(deployScript).toContain('RUN_MIGRATIONS_ON_STARTUP');
expect(deployScript.indexOf(migrationCommand)).toBeLessThan(
@ -78,5 +84,10 @@ describe('performance migrations', () => {
expect(composeFile).toContain(
'RUN_MIGRATIONS_ON_STARTUP=${RUN_MIGRATIONS_ON_STARTUP:-false}',
);
expect(composeFile).toContain(
'psql -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -c "SELECT 1"',
);
expect(composeFile).toContain('aliases:');
expect(composeFile).toContain('- db');
});
});