#!/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