# MotorSignal OPS-4 Production Command Card *(Hardened Operator Companion Card)* > **OPERATOR COMPANION CARD — DOCUMENTATION & PLANNING ONLY** > Checkpoint Commit: `8681e6f25972cea768106e534b11418b58374d28` > PostgreSQL Engine: `PostgreSQL 14.24` | Database: `motorsignal` (User: `motorsignal_app`) > Baseline Alembic Revision: `e0f5a7123b46` $\to$ Target Alembic Revision: `f1a2b3c4d5e6` > Authoritative Runbook: `docs/OPS4_PRODUCTION_DEPLOYMENT_RUNBOOK.md` --- ## 1. Preflight & Release Artifact Verification ```bash # 1. SSH into host & record current active release ssh -i ~/.ssh/motorsignal_prod motorsignal-admin@89.31.239.10 PREV_RELEASE=$(readlink -f /opt/motorsignal/current) echo "PREVIOUS_RELEASE=${PREV_RELEASE}" # 2. Stage new release directory & unpack artifacts RELEASE_ID="20260910-ops4" NEW_RELEASE="/opt/motorsignal/releases/${RELEASE_ID}" sudo install -d -o motorsignal -g motorsignal "${NEW_RELEASE}/backend" "${NEW_RELEASE}/website" sudo -u motorsignal unzip -q /tmp/motorsignal-backend-${RELEASE_ID}.zip -d "${NEW_RELEASE}/backend" sudo -u motorsignal unzip -q /tmp/motorsignal-website-${RELEASE_ID}.zip -d "${NEW_RELEASE}/website" # 3. Create compatibility symlinks & dedicated venv sudo -u motorsignal ln -sfn backend/app "${NEW_RELEASE}/app" sudo -u motorsignal ln -sfn backend/alembic "${NEW_RELEASE}/alembic" sudo -u motorsignal ln -sfn backend/alembic.ini "${NEW_RELEASE}/alembic.ini" sudo -u motorsignal ln -sfn backend/requirements.lock "${NEW_RELEASE}/requirements.lock" sudo -u motorsignal python3 -m venv "${NEW_RELEASE}/venv" sudo -u motorsignal "${NEW_RELEASE}/venv/bin/pip" install --no-cache-dir -r "${NEW_RELEASE}/requirements.lock" # 4. Mandatory Artifact Preflight Gate "${NEW_RELEASE}/venv/bin/alembic" -c "${NEW_RELEASE}/alembic.ini" heads # MUST output exactly: f1a2b3c4d5e6 (head) sudo -u postgres psql -d motorsignal -t -A -c "SELECT version_num FROM alembic_version;" # MUST output exactly: e0f5a7123b46 ``` --- ## 2. Safe Pre-Migration Backup & Restore Drill ```bash # 1. Execute safe custom-format backup (No passwords in argv) sudo install -d -m 0750 -o postgres -g postgres /var/backups/motorsignal BACKUP_TS=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="/var/backups/motorsignal/motorsignal_pre_ops4_${BACKUP_TS}.dump" export BACKUP_FILE sudo -u motorsignal BACKUP_FILE="${BACKUP_FILE}" "${NEW_RELEASE}/venv/bin/python" - <<'PY' import os, subprocess, sys from dotenv import dotenv_values env_vars = dotenv_values("/etc/motorsignal/production.env") pg_env = os.environ.copy() pg_env["PGHOST"] = env_vars.get("DATABASE_HOST", "127.0.0.1") pg_env["PGPORT"] = str(env_vars.get("DATABASE_PORT", "5432")) pg_env["PGDATABASE"] = env_vars.get("DATABASE_NAME", "motorsignal") pg_env["PGUSER"] = env_vars.get("DATABASE_USER", "motorsignal_app") if "DATABASE_PASSWORD" in env_vars: pg_env["PGPASSWORD"] = env_vars["DATABASE_PASSWORD"] res = subprocess.run(["pg_dump", "-Fc", "-v", "-f", os.environ["BACKUP_FILE"], pg_env["PGDATABASE"]], env=pg_env) sys.exit(res.returncode) PY test -s "${BACKUP_FILE}" && sha256sum "${BACKUP_FILE}" | sudo tee "${BACKUP_FILE}.sha256" sudo -u postgres pg_restore --list "${BACKUP_FILE}" | head -n 20 # 2. Disposable Restore Drill (Mandatory Gate) DRILL_DB="motorsignal_restore_drill_${BACKUP_TS}" sudo -u postgres psql -c "CREATE DATABASE ${DRILL_DB};" sudo -u postgres pg_restore -v --no-owner --no-privileges -d "${DRILL_DB}" "${BACKUP_FILE}" sudo -u postgres psql -d "${DRILL_DB}" -t -A -c "SELECT version_num FROM alembic_version;" # MUST output: e0f5a7123b46 sudo -u postgres psql -c "DROP DATABASE ${DRILL_DB};" ``` --- ## 3. Quiesce & Final Backup ```bash # Quiesce application writers (Nginx remains active) sudo systemctl stop motorsignal-api.service motorsignal-website.service sudo systemctl is-active motorsignal-api.service || echo "API_STOPPED" sudo systemctl is-active postgresql@14-main.service # Final quiesced snapshot FINAL_BACKUP_FILE="/var/backups/motorsignal/motorsignal_final_quiesced_${BACKUP_TS}.dump" sudo -u motorsignal BACKUP_FILE="${FINAL_BACKUP_FILE}" "${NEW_RELEASE}/venv/bin/python" - <<'PY' import os, subprocess, sys from dotenv import dotenv_values env_vars = dotenv_values("/etc/motorsignal/production.env") pg_env = os.environ.copy() pg_env["PGHOST"] = env_vars.get("DATABASE_HOST", "127.0.0.1") pg_env["PGPORT"] = str(env_vars.get("DATABASE_PORT", "5432")) pg_env["PGDATABASE"] = env_vars.get("DATABASE_NAME", "motorsignal") pg_env["PGUSER"] = env_vars.get("DATABASE_USER", "motorsignal_app") if "DATABASE_PASSWORD" in env_vars: pg_env["PGPASSWORD"] = env_vars["DATABASE_PASSWORD"] res = subprocess.run(["pg_dump", "-Fc", "-v", "-f", os.environ["BACKUP_FILE"], pg_env["PGDATABASE"]], env=pg_env) sys.exit(res.returncode) PY test -s "${FINAL_BACKUP_FILE}" && sha256sum "${FINAL_BACKUP_FILE}" | sudo tee "${FINAL_BACKUP_FILE}.sha256" ``` --- ## 4. Single-Execution Database Migration ```bash # Execute Alembic upgrade safely via Python runner (STOP IMMEDIATELY ON ERROR) sudo -u motorsignal NEW_RELEASE="${NEW_RELEASE}" "${NEW_RELEASE}/venv/bin/python" - <<'PY' import os, subprocess, sys from dotenv import dotenv_values release = os.environ["NEW_RELEASE"] env_vars = dotenv_values("/etc/motorsignal/production.env") run_env = os.environ.copy() for k, v in env_vars.items(): if v is not None: run_env[k] = v res = subprocess.run( [f"{release}/venv/bin/alembic", "-c", f"{release}/alembic.ini", "upgrade", "f1a2b3c4d5e6"], cwd=release, env=run_env ) sys.exit(res.returncode) PY # STOP IMMEDIATELY if process exits non-zero! ``` --- ## 5. Post-Migration DB Verification ```bash # Verify Alembic version is f1a2b3c4d5e6 sudo -u postgres psql -d motorsignal -t -A -c "SELECT version_num FROM alembic_version;" # Verify ops schema & 5 tables sudo -u postgres psql -d motorsignal -c " SELECT table_name FROM information_schema.tables WHERE table_schema = 'ops' ORDER BY table_name;" # Verify sequence, function, and trigger sudo -u postgres psql -d motorsignal -c " SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'ops'; SELECT routine_name FROM information_schema.routines WHERE routine_schema = 'ops'; SELECT trigger_name, event_object_table FROM information_schema.triggers WHERE trigger_schema = 'ops';" ``` --- ## 6. Atomic Symlink Cutover & Service Start ```bash # 1. Pre-check: current.next must not exist test ! -e /opt/motorsignal/current.next || { echo "STOP: current.next exists!"; exit 1; } # 2. Create temporary symlink & verify sudo -u motorsignal ln -s "${NEW_RELEASE}" /opt/motorsignal/current.next test "$(readlink -f /opt/motorsignal/current.next)" = "${NEW_RELEASE}" || { echo "STOP: target mismatch!"; exit 1; } # 3. Truly atomic cutover via Linux rename sudo mv -Tf /opt/motorsignal/current.next /opt/motorsignal/current test "$(readlink -f /opt/motorsignal/current)" = "${NEW_RELEASE}" && echo "CUTOVER_OK" # 4. Start services sudo systemctl start motorsignal-api.service motorsignal-website.service sudo systemctl status motorsignal-api.service motorsignal-website.service --no-pager ``` --- ## 7. Local & Public Health Checks ```bash # Local loopback probes curl -fsS http://127.0.0.1:8080/api/v1/health curl -fsS http://127.0.0.1:8080/api/v1/ready curl -fsS http://127.0.0.1:8080/api/v1/ops/health curl -fsS -I http://127.0.0.1:3001/ # Public HTTPS probes curl -fsS https://api.motorsignal.app/api/v1/health curl -fsS https://api.motorsignal.app/api/v1/ready curl -fsS -I https://motorsignal.app/ # Safe unauthenticated security probes (Must return HTTP 401, NOT 404) curl -s -o /dev/null -w "%{http_code}\n" https://api.motorsignal.app/api/v1/ops/me curl -s -o /dev/null -w "%{http_code}\n" https://api.motorsignal.app/api/v1/ops/incidents ``` --- ## 8. Rollback Triggers & Fast Action ### Case A: Application Failure (No New Writes) ```bash # 1. Stop services sudo systemctl stop motorsignal-api.service motorsignal-website.service # 2. Atomically repoint symlink to previous release sudo -u motorsignal ln -s "${PREV_RELEASE}" /opt/motorsignal/current.prev sudo mv -Tf /opt/motorsignal/current.prev /opt/motorsignal/current # 3. Restart previous services & verify sudo systemctl start motorsignal-api.service motorsignal-website.service curl -fsS http://127.0.0.1:8080/api/v1/health && curl -fsS http://127.0.0.1:8080/api/v1/ready ``` ### Case B: Migration Non-Zero Exit - **DO NOT** restart application services. - **DO NOT** run `alembic stamp`. - **DO NOT** retry repeatedly. - Keep writers stopped; inspect `journalctl` and PostgreSQL logs.