Skip to content

Commit 39d865d

Browse files
committed
feat: docker-entrypoint pre-boot DB connectivity check
The entrypoint now tests the database connection BEFORE Rails boots. On failure, prints plain-English diagnostics with exact fix commands instead of a PG::ConnectionBad Ruby backtrace. Uses pg_isready (TCP + PG protocol, no auth needed) with psql fallback. No Rails, no Ruby — works even when the Rails environment itself can't load due to a misconfigured connection. Diagnostics cover the 5 most common connection failures: 1. Wrong hostname (Docker service name vs localhost vs Aurora cluster endpoint) 2. Missing sslmode=require (Aurora/RDS enforce SSL) 3. GSSAPI negotiation failure (DATABASE_GSSENCMODE=disable) 4. Database server not running / firewall blocking 5. Wrong credentials On success, proceeds to db:prepare as before. Tested: bad host prints diagnostics + exits 1; good host prints "Database is reachable" + continues to db:prepare. Shellcheck clean. Authored by: Aaron Lippold<lippold@gmail.com>
1 parent 342cdac commit 39d865d

2 files changed

Lines changed: 137 additions & 9 deletions

File tree

bin/docker-entrypoint

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,137 @@
33
# Remove stale PID file if present (prevents "server is already running" errors)
44
rm -f /rails/tmp/pids/server.pid
55

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.
911
#
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
15137
./bin/rails db:prepare
16138
fi
17139

docs/deployment/upgrade-guide.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Vulcan Upgrade Guide
22

3+
## How it works
4+
5+
From v2.3.6+, the Docker entrypoint automatically tests the database connection **before** Rails boots. If the connection fails, you get a plain-English diagnostic with exact fix commands — not a Ruby backtrace.
6+
7+
If the connection succeeds, `db:prepare` runs pending migrations and the server starts.
8+
39
## Quick Start
410

5-
The upgrade toolkit is built into every Vulcan image from v2.3.6+. No file injection, no extra installs — just pull and run.
11+
Just pull the new image and start. The container tells you what's wrong.
612

713
### Docker Compose (most common)
814

0 commit comments

Comments
 (0)