Files
Momento/memento-note/docker-entrypoint.sh
sepehr 39c705592a
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
feat: robust automatic DB migration for Docker deployments
Backup before migration (pg_dump/SQLite copy), DB connection wait with
retries, idempotent prisma migrate deploy, old backup cleanup (keep 5),
and server refuses to start if migration fails.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:30:45 +02:00

169 lines
5.1 KiB
Bash

#!/bin/sh
# ============================================================
# Memento Note — Docker Entrypoint
# Automatic DB migration with backup, retry, and cleanup.
# Safe for production: no data loss on image updates.
# ============================================================
set -e
BACKUP_DIR="/app/data/backups"
MAX_BACKUPS=5
DB_WAIT_RETRIES=30
DB_WAIT_INTERVAL=2
MIGRATE_TIMEOUT=120
# --- Logging ---
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [entrypoint] $*"; }
warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [entrypoint] WARNING: $*" >&2; }
err() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [entrypoint] ERROR: $*" >&2; }
# --- Detect database type ---
DB_TYPE="unknown"
case "$DATABASE_URL" in
postgres://*|postgresql://*) DB_TYPE="postgres" ;;
file:*) DB_TYPE="sqlite" ;;
esac
log "Database type: $DB_TYPE"
# --- Wait for database to be reachable ---
wait_for_db() {
if [ "$DB_TYPE" = "sqlite" ]; then
log "SQLite — no connection check needed."
return 0
fi
log "Waiting for database connection..."
i=0
while [ "$i" -lt "$DB_WAIT_RETRIES" ]; do
if node -e "
const m = process.env.DATABASE_URL.match(/@([^:\/]+):(\d+)/);
if (!m) process.exit(1);
const net = require('net');
const s = net.createConnection(parseInt(m[2]), m[1], () => { s.end(); process.exit(0); });
s.setTimeout(3000, () => { s.destroy(); process.exit(1); });
s.on('error', () => process.exit(1));
" 2>/dev/null; then
log "Database is reachable."
return 0
fi
i=$((i + 1))
log " Attempt $i/$DB_WAIT_RETRIES — retrying in ${DB_WAIT_INTERVAL}s..."
sleep "$DB_WAIT_INTERVAL"
done
err "Database unreachable after $((DB_WAIT_RETRIES * DB_WAIT_INTERVAL)) seconds. Aborting."
exit 1
}
# --- Create backup before migration ---
create_backup() {
mkdir -p "$BACKUP_DIR"
ts=$(date '+%Y%m%d_%H%M%S')
case "$DB_TYPE" in
postgres)
backup_file="$BACKUP_DIR/pre_migrate_${ts}.sql.gz"
log "Creating PostgreSQL backup: $backup_file"
if command -v pg_dump >/dev/null 2>&1; then
if pg_dump --no-owner --no-privileges --format=plain "$DATABASE_URL" 2>/dev/null | gzip > "$backup_file" 2>/dev/null; then
size=$(du -h "$backup_file" | cut -f1)
log "Backup created successfully ($size)"
else
warn "pg_dump failed. Continuing without backup."
rm -f "$backup_file"
fi
else
warn "pg_dump not available. Skipping PostgreSQL backup."
warn "Install postgresql-client in the Docker image for automatic backups."
fi
;;
sqlite)
db_path="${DATABASE_URL#file:}"
# Handle relative paths
case "$db_path" in
/*) ;;
*) db_path="/app/$db_path" ;;
esac
backup_file="$BACKUP_DIR/pre_migrate_${ts}.sqlite"
if [ -f "$db_path" ]; then
log "Creating SQLite backup: $backup_file"
cp "$db_path" "$backup_file"
size=$(du -h "$backup_file" | cut -f1)
log "Backup created successfully ($size)"
else
warn "SQLite file not found at $db_path — skipping backup (first run)."
fi
;;
*)
warn "Unknown database type '$DB_TYPE'. Skipping backup."
;;
esac
}
# --- Clean up old backups (keep last N) ---
cleanup_old_backups() {
count=$(ls -1 "$BACKUP_DIR"/pre_migrate_* 2>/dev/null | wc -l)
if [ "$count" -gt "$MAX_BACKUPS" ]; then
to_remove=$((count - MAX_BACKUPS))
log "Cleaning up $to_remove old backup(s), keeping last $MAX_BACKUPS..."
ls -1t "$BACKUP_DIR"/pre_migrate_* 2>/dev/null | tail -n "$to_remove" | xargs rm -f
fi
}
# --- Run Prisma migrations ---
run_migrations() {
log "Running Prisma migrations..."
if timeout "$MIGRATE_TIMEOUT" node ./node_modules/prisma/build/index.js migrate deploy 2>&1; then
log "Migrations applied successfully."
return 0
else
exit_code=$?
err "Migration failed (exit code $exit_code)."
return "$exit_code"
fi
}
# ============================================================
# Main flow
# ============================================================
# Step 1: Wait for database
wait_for_db
# Step 2: Backup
create_backup
# Step 3: Cleanup old backups
cleanup_old_backups
# Step 4: Migrate
if ! run_migrations; then
err "Migration failed — server will NOT start to prevent data corruption."
err "A backup was saved in $BACKUP_DIR (if backup was successful)."
err "To restore: gunzip the .sql.gz file, then: psql DATABASE_URL < backup.sql"
exit 1
fi
# Background scheduler: call /api/cron/agents every 5 minutes
(
sleep 60
while true; do
node -e "
const http = require('http');
const req = http.request('http://localhost:3000/api/cron/agents', { method: 'POST' }, (res) => {
let body = '';
res.on('data', (d) => body += d);
res.on('end', () => console.log('[Scheduler] Cron response:', res.statusCode, body));
});
req.on('error', (e) => console.error('[Scheduler] Cron error:', e.message));
req.end();
" 2>&1 || true
sleep 300
done
) &
# Step 5: Start server
log "Starting server..."
exec node server.js