#!/bin/sh # ============================================================ # Memento Note — Docker Entrypoint # Reliable DB migration for fresh installs and upgrades. # ============================================================ # Strategy: # 1. prisma migrate deploy → fresh DB, normal upgrades # 2. prisma db push → fallback for DBs without migration history # ============================================================ 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" # --- Ensure data directories exist --- mkdir -p "$BACKUP_DIR" 2>/dev/null || true mkdir -p /app/data/uploads/notes 2>/dev/null || true # --- Wait for database to be reachable --- if [ "$DB_TYPE" != "sqlite" ]; then 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." break fi i=$((i + 1)) log " Attempt $i/$DB_WAIT_RETRIES — retrying in ${DB_WAIT_INTERVAL}s..." sleep "$DB_WAIT_INTERVAL" done if [ "$i" -ge "$DB_WAIT_RETRIES" ]; then err "Database unreachable after $((DB_WAIT_RETRIES * DB_WAIT_INTERVAL)) seconds." exit 1 fi fi # --- Create backup before migration (best-effort) --- if [ "$DB_TYPE" = "postgres" ] && command -v pg_dump >/dev/null 2>&1; then ts=$(date '+%Y%m%d_%H%M%S') backup_file="$BACKUP_DIR/pre_migrate_${ts}.sql.gz" log "Creating backup: $backup_file" 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 OK ($size)" else warn "pg_dump failed (DB may be empty). Continuing without backup." rm -f "$backup_file" fi fi # --- 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)..." ls -1t "$BACKUP_DIR"/pre_migrate_* 2>/dev/null | tail -n "$to_remove" | xargs rm -f fi # ============================================================ # Migration — two strategies # ============================================================ PRISMA="node ./node_modules/prisma/build/index.js" log "Running database migrations..." # Strategy 1: prisma migrate deploy # Works for: fresh DB (empty), existing DB with migration history if timeout "$MIGRATE_TIMEOUT" $PRISMA migrate deploy 2>&1; then log "Migrations applied successfully." else migrate_exit=$? log "prisma migrate deploy failed (exit $migrate_exit)." # Strategy 2: prisma db push # Works for: existing DB without migration history (created with db push), # schema drift, or any other case where migrate deploy fails. # This syncs schema.prisma → database, preserving existing data. log "Falling back to prisma db push..." if timeout "$MIGRATE_TIMEOUT" $PRISMA db push --skip-generate --accept-data-loss 2>&1; then log "db push completed successfully." else err "All migration strategies failed." err "To fix manually:" err " docker compose exec memento-note npx prisma migrate deploy" err " docker compose exec memento-note npx prisma db push --skip-generate" exit 1 fi fi # --- Background cron scheduler --- ( 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 ) & # --- Start server --- log "Starting server..." exec node server.js