From d6f8f55c092baf960c407d6495ae89b58dd8848f Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 9 Jul 2026 16:06:02 +0200 Subject: [PATCH] feat: add migration support for VPS deployment with backup functionality --- docs/vps-deployment.md | 28 ++++++++++++++++++++++++++++ package.json | 1 + scripts/deploy-vps.sh | 22 +++++++++++++++++++++- scripts/vps-migrate.sh | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100755 scripts/vps-migrate.sh diff --git a/docs/vps-deployment.md b/docs/vps-deployment.md index 13d7924..d153c28 100644 --- a/docs/vps-deployment.md +++ b/docs/vps-deployment.md @@ -120,6 +120,34 @@ If the Caddy container also needs to be recreated: DEPLOY_CADDY=1 pnpm deploy:vps ``` +## Payload Migrations + +Routine deploys do not run migrations. For schema-changing commits, create and +commit the Payload migration files, then deploy with migrations enabled: + +```bash +RUN_MIGRATIONS=1 pnpm deploy:vps +``` + +That mode: + +1. syncs the committed code +2. builds the new Docker image +3. stops the app container +4. copies `shared/content.db` to `backups/content-*-pre-migrate.db` +5. runs `pnpm payload migrate` in a one-off app container +6. starts the app container again + +To run migrations against the already deployed code/image without syncing code: + +```bash +pnpm vps:migrate +``` + +Run migrations only when the commit includes migration files under +`src/migrations/`. For plain frontend/content-rendering changes, use the normal +`pnpm deploy:vps` path. + ## Status And Logs ```bash diff --git a/package.json b/package.json index 9c07121..feca180 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" playwright test --config=playwright.config.ts", "test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts", "vps:backup-db": "bash scripts/vps-backup-db.sh", + "vps:migrate": "bash scripts/vps-migrate.sh", "vps:rollback:n8n": "bash scripts/vps-rollback-n8n.sh", "vps:status": "bash scripts/vps-status.sh" }, diff --git a/scripts/deploy-vps.sh b/scripts/deploy-vps.sh index c6bba84..e0a6b9a 100755 --- a/scripts/deploy-vps.sh +++ b/scripts/deploy-vps.sh @@ -13,6 +13,7 @@ APP_DOMAIN="${APP_DOMAIN:-srv1.bayerischer-mittelstandspreis.de}" PUBLIC_URL="${PUBLIC_URL:-https://$APP_DOMAIN}" ALLOW_DIRTY="${ALLOW_DIRTY:-0}" DEPLOY_CADDY="${DEPLOY_CADDY:-0}" +RUN_MIGRATIONS="${RUN_MIGRATIONS:-0}" SKIP_VERIFY="${SKIP_VERIFY:-0}" SSH_TARGET="$SSH_USER@$VPS_HOST" @@ -53,7 +54,26 @@ ssh -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " set -e cd '$APP_ROOT' APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' build app - APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' up -d app + if [ '$RUN_MIGRATIONS' = '1' ]; then + backup_dir='$APP_ROOT/backups' + backup_file=\"\$backup_dir/content-\$(date +%Y%m%d-%H%M%S)-pre-migrate.db\" + + sudo install -d -o '$SSH_USER' -g '$SSH_USER' \"\$backup_dir\" + + restart_app() { + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' up -d app >/dev/null + } + + trap restart_app EXIT + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' stop app >/dev/null + cp -p '$APP_ROOT/shared/content.db' \"\$backup_file\" + echo \"Created pre-migration backup: \$backup_file\" + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' run --rm --no-deps app pnpm payload migrate + restart_app + trap - EXIT + else + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' up -d app + fi if [ '$DEPLOY_CADDY' = '1' ]; then APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' up -d caddy fi diff --git a/scripts/vps-migrate.sh b/scripts/vps-migrate.sh new file mode 100755 index 0000000..a051ad5 --- /dev/null +++ b/scripts/vps-migrate.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +SSH_USER="${SSH_USER:-syntaxbullet}" +VPS_HOST="${VPS_HOST:-srv1.bayerischer-mittelstandspreis.de}" +VPS_PORT="${VPS_PORT:-22}" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/bmp-vps-deploy}" +APP_ROOT="${APP_ROOT:-/var/www/bmp-website}" +APP_DOMAIN="${APP_DOMAIN:-srv1.bayerischer-mittelstandspreis.de}" + +SSH_TARGET="$SSH_USER@$VPS_HOST" +COMPOSE_FILE="$APP_ROOT/current/deploy/vps/docker-compose.yml" + +ssh -i "$SSH_KEY" -p "$VPS_PORT" "$SSH_TARGET" " + set -e + cd '$APP_ROOT' + + backup_dir='$APP_ROOT/backups' + backup_file=\"\$backup_dir/content-\$(date +%Y%m%d-%H%M%S)-pre-migrate.db\" + + sudo install -d -o '$SSH_USER' -g '$SSH_USER' \"\$backup_dir\" + + restart_app() { + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' up -d app >/dev/null + } + + trap restart_app EXIT + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' stop app >/dev/null + cp -p '$APP_ROOT/shared/content.db' \"\$backup_file\" + echo \"Created pre-migration backup: \$backup_file\" + APP_ROOT='$APP_ROOT' APP_DOMAIN='$APP_DOMAIN' sudo docker compose -f '$COMPOSE_FILE' run --rm --no-deps app pnpm payload migrate + restart_app + trap - EXIT + + sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' +"