|
3 | 3 | # Remove stale PID file if present (prevents "server is already running" errors) |
4 | 4 | rm -f /rails/tmp/pids/server.pid |
5 | 5 |
|
6 | | -# If running the rails server then create or migrate existing database |
7 | | -# db:prepare is idempotent: creates DB if missing, runs pending migrations if exists |
8 | | -# This also triggers admin:bootstrap (hooked into db:prepare) |
| 6 | +# ─── Pre-boot database connectivity check ───────────────────────────── |
| 7 | +# Runs BEFORE Rails boots. Uses pg_isready (no Ruby, no Rails) so it |
| 8 | +# works even when the Rails environment itself can't load due to a |
| 9 | +# misconfigured database connection. On failure, prints diagnostics |
| 10 | +# with exact fix commands instead of a raw PG::ConnectionBad backtrace. |
9 | 11 | # |
10 | | -# IMPORTANT: db:prepare does NOT need DISABLE_DATABASE_ENVIRONMENT_CHECK=1 |
11 | | -# Unlike db:schema:load (rake task), db:prepare calls load_schema() directly |
12 | | -# as a Ruby method, bypassing the protected environment check entirely. |
13 | | -# This is by design — see Rails source: DatabaseTasks#initialize_database |
14 | | -if [[ "$@" == *"server"* ]]; then |
| 12 | +# This is the "gas and go" safety net: if the container can't reach the |
| 13 | +# database, the operator sees actionable output within 10 seconds |
| 14 | +# instead of a cryptic Rails crash. |
| 15 | +# ────────────────────────────────────────────────────────────────────── |
| 16 | +check_database_connectivity() { |
| 17 | + # Build connection params from the same env vars that database.yml reads |
| 18 | + if [ -n "$DATABASE_URL" ]; then |
| 19 | + DB_CHECK_URL="$DATABASE_URL" |
| 20 | + else |
| 21 | + local host="${DATABASE_HOST:-127.0.0.1}" |
| 22 | + local port="${DATABASE_PORT:-5432}" |
| 23 | + local user="${POSTGRES_USER:-postgres}" |
| 24 | + local dbname="${POSTGRES_DB:-vulcan_postgres_production}" |
| 25 | + DB_CHECK_URL="postgres://${user}@${host}:${port}/${dbname}" |
| 26 | + fi |
| 27 | + |
| 28 | + # Extract host:port for pg_isready (handles both URL and individual vars) |
| 29 | + local check_host check_port check_user |
| 30 | + if [ -n "$DATABASE_URL" ]; then |
| 31 | + check_host=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+).*|\1|') |
| 32 | + check_port=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|') |
| 33 | + check_user=$(echo "$DATABASE_URL" | sed -E 's|.*://([^:@]+).*|\1|') |
| 34 | + [ -z "$check_port" ] && check_port=5432 |
| 35 | + else |
| 36 | + check_host="${DATABASE_HOST:-127.0.0.1}" |
| 37 | + check_port="${DATABASE_PORT:-5432}" |
| 38 | + check_user="${POSTGRES_USER:-postgres}" |
| 39 | + fi |
| 40 | + |
| 41 | + echo "Checking database connectivity (${check_host}:${check_port})..." |
| 42 | + |
| 43 | + # pg_isready is a lightweight TCP + PG protocol check — no auth needed |
| 44 | + if command -v pg_isready &>/dev/null; then |
| 45 | + if pg_isready -h "$check_host" -p "$check_port" -U "$check_user" -t 10 &>/dev/null; then |
| 46 | + echo " ✓ Database is reachable" |
| 47 | + return 0 |
| 48 | + fi |
| 49 | + fi |
| 50 | + |
| 51 | + # pg_isready failed or not available — try a psql connection test |
| 52 | + if command -v psql &>/dev/null; then |
| 53 | + if psql "$DB_CHECK_URL" -c "SELECT 1" &>/dev/null 2>&1; then |
| 54 | + echo " ✓ Database is reachable" |
| 55 | + return 0 |
| 56 | + fi |
| 57 | + fi |
| 58 | + |
| 59 | + # ── Connection failed — print diagnostics ── |
| 60 | + echo "" |
| 61 | + echo "======================================================================" |
| 62 | + echo " ✗ DATABASE CONNECTION FAILED" |
| 63 | + echo "======================================================================" |
| 64 | + echo "" |
| 65 | + echo " Vulcan cannot reach the database at ${check_host}:${check_port}" |
| 66 | + echo "" |
| 67 | + echo " ── Check these first ──" |
| 68 | + echo "" |
| 69 | + if [ -n "$DATABASE_URL" ]; then |
| 70 | + echo " DATABASE_URL is set: ${check_host}:${check_port}" |
| 71 | + else |
| 72 | + echo " DATABASE_URL is NOT set (using individual env vars)" |
| 73 | + echo " DATABASE_HOST=${DATABASE_HOST:-(not set, defaulting to 127.0.0.1)}" |
| 74 | + echo " DATABASE_PORT=${DATABASE_PORT:-(not set, defaulting to 5432)}" |
| 75 | + echo " POSTGRES_USER=${POSTGRES_USER:-(not set, defaulting to postgres)}" |
| 76 | + echo " POSTGRES_DB=${POSTGRES_DB:-(not set, defaulting to vulcan_postgres_production)}" |
| 77 | + fi |
| 78 | + echo "" |
| 79 | + echo " ── Common fixes ──" |
| 80 | + echo "" |
| 81 | + echo " 1. Is the database host correct?" |
| 82 | + echo " • Docker Compose: use 'db' (the service name), not 'localhost'" |
| 83 | + echo " • Aurora RDS: use the CLUSTER endpoint, not the instance endpoint" |
| 84 | + echo " • Kubernetes: use the service DNS name" |
| 85 | + echo "" |
| 86 | + echo " 2. Is SSL required?" |
| 87 | + echo " • Aurora/RDS/Cloud SQL require SSL by default" |
| 88 | + echo " • Add ?sslmode=require to DATABASE_URL:" |
| 89 | + echo " DATABASE_URL=postgres://user:pass@host:5432/dbname?sslmode=require" |
| 90 | + echo "" |
| 91 | + echo " 3. Is GSSAPI causing issues?" |
| 92 | + echo " • Aurora does not support GSSAPI authentication" |
| 93 | + echo " • Set: DATABASE_GSSENCMODE=disable" |
| 94 | + echo "" |
| 95 | + echo " 4. Is the database server running?" |
| 96 | + echo " • Docker Compose: check 'docker compose ps db'" |
| 97 | + echo " • Aurora: check the RDS console for cluster status" |
| 98 | + echo " • Firewall/security group blocking port ${check_port}?" |
| 99 | + echo "" |
| 100 | + echo " 5. Are credentials correct?" |
| 101 | + echo " • Check POSTGRES_USER and POSTGRES_PASSWORD in .env" |
| 102 | + echo " • Aurora IAM auth requires a different connection method" |
| 103 | + echo "" |
| 104 | + echo " ── Quick test from this container ──" |
| 105 | + echo "" |
| 106 | + echo " docker compose exec web bash" |
| 107 | + if [ -n "$DATABASE_URL" ]; then |
| 108 | + echo " pg_isready -h ${check_host} -p ${check_port}" |
| 109 | + else |
| 110 | + echo " pg_isready -h \$DATABASE_HOST -p \$DATABASE_PORT" |
| 111 | + fi |
| 112 | + echo "" |
| 113 | + echo " ── Standalone diagnostic (no Rails needed) ──" |
| 114 | + echo "" |
| 115 | + echo " curl -fsSL https://raw.githubusercontent.com/mitre/vulcan/master/bin/upgrade-check.sh | bash -s -- \"\$DATABASE_URL\"" |
| 116 | + echo "" |
| 117 | + echo "======================================================================" |
| 118 | + echo "" |
| 119 | + return 1 |
| 120 | +} |
| 121 | + |
| 122 | +if [[ "$*" == *"server"* ]]; then |
| 123 | + # Check connectivity BEFORE Rails boots — gives actionable diagnostics |
| 124 | + # instead of a raw PG::ConnectionBad backtrace |
| 125 | + if ! check_database_connectivity; then |
| 126 | + echo "Exiting. Fix the database connection and restart the container." |
| 127 | + exit 1 |
| 128 | + fi |
| 129 | + |
| 130 | + # db:prepare is idempotent: creates DB if missing, runs pending migrations if exists |
| 131 | + # This also triggers admin:bootstrap (hooked into db:prepare) |
| 132 | + # |
| 133 | + # IMPORTANT: db:prepare does NOT need DISABLE_DATABASE_ENVIRONMENT_CHECK=1 |
| 134 | + # Unlike db:schema:load (rake task), db:prepare calls load_schema() directly |
| 135 | + # as a Ruby method, bypassing the protected environment check entirely. |
| 136 | + # This is by design — see Rails source: DatabaseTasks#initialize_database |
15 | 137 | ./bin/rails db:prepare |
16 | 138 | fi |
17 | 139 |
|
|
0 commit comments