changemaker.lite/api/docker-entrypoint.sh
bunker-admin 530551f568 Fix deployment issues found during end-to-end testing
- install.sh: Use tar --strip-components=1 instead of mv for robust
  extraction when install dir partially exists (root-owned Docker
  artifacts)
- config.sh: Add --non-interactive mode (--domain, --admin-password,
  --enable-all flags) for CI/CD and automated deployments
- docker-entrypoint.sh: Validate critical env vars on startup, fail
  early with clear messages instead of silent failures
- docker-compose.yml: Change Redis eviction policy from allkeys-lru
  to noeviction (required by BullMQ job queues)
- Prisma: Add missing petitions.coverVideoId migration (schema had
  the column but migration omitted it, causing 500 on public endpoint)
- Add scripts/uninstall.sh for clean removal including root-owned files
- Add scripts/test-deployment.sh for automated post-install verification

Bunker Admin
2026-04-07 14:06:05 -06:00

122 lines
4.9 KiB
Bash
Executable File

#!/bin/sh
set -e
# Block NODE_TLS_REJECT_UNAUTHORIZED=0 in production
if [ "$NODE_ENV" = "production" ] && [ "$NODE_TLS_REJECT_UNAUTHORIZED" = "0" ]; then
echo "FATAL: NODE_TLS_REJECT_UNAUTHORIZED=0 is not allowed in production"
exit 1
fi
# Validate critical environment variables
ENV_ERRORS=0
check_env() {
if [ -z "$2" ] || echo "$2" | grep -q "REQUIRED_STRONG_PASSWORD\|GENERATE_WITH_openssl"; then
echo "FATAL: $1 is not set or contains a placeholder value"
ENV_ERRORS=$((ENV_ERRORS + 1))
fi
}
check_env "DATABASE_URL" "$DATABASE_URL"
check_env "REDIS_URL" "$REDIS_URL"
check_env "JWT_ACCESS_SECRET" "$JWT_ACCESS_SECRET"
check_env "JWT_REFRESH_SECRET" "$JWT_REFRESH_SECRET"
check_env "ENCRYPTION_KEY" "$ENCRYPTION_KEY"
check_env "INITIAL_ADMIN_PASSWORD" "$INITIAL_ADMIN_PASSWORD"
if [ "$ENV_ERRORS" -gt 0 ]; then
echo ""
echo "FATAL: $ENV_ERRORS required environment variable(s) missing or invalid."
echo "Run the configuration wizard: bash config.sh"
echo "Or set them manually in .env and restart."
exit 1
fi
# Fix permissions for mounted volumes (host dirs may be root-owned on first run)
if [ "$(id -u)" = "0" ]; then
mkdir -p /app/logs /data/geoip /app/uploads 2>/dev/null || true
chown -R node:node /app/logs /data/geoip /app/uploads 2>/dev/null || true
fi
# Wait for PostgreSQL to be ready before running migrations
echo "Waiting for database..."
MAX_WAIT=30
WAITED=0
until echo "SELECT 1" | npx prisma db execute --stdin --schema ./prisma/schema.prisma 2>/dev/null; do
sleep 2
WAITED=$((WAITED + 2))
if [ $WAITED -ge $MAX_WAIT ]; then
echo "FATAL: Database not available after ${MAX_WAIT}s"
exit 1
fi
done
echo "Database ready (${WAITED}s)"
# Run migrations — fail hard on error (never fall back to db push, which causes drift)
echo "Running Prisma migrations..."
npx prisma migrate deploy 2>&1
echo "Migrations complete."
echo "Running database seed..."
npx prisma db seed 2>&1 || echo "WARNING: Seed failed (non-fatal — seed.ts may require source files not present in production image)"
echo "Seed step done."
# Download MaxMind GeoLite2 database if credentials are provided and DB is missing/stale
if [ -n "$MAXMIND_ACCOUNT_ID" ] && [ -n "$MAXMIND_LICENSE_KEY" ]; then
GEOIP_DIR="/data/geoip"
GEOIP_DB="$GEOIP_DIR/GeoLite2-City.mmdb"
mkdir -p "$GEOIP_DIR" 2>/dev/null || true
# Re-download weekly (if file is older than 7 days or missing)
if [ ! -f "$GEOIP_DB" ] || [ "$(find "$GEOIP_DB" -mtime +7 2>/dev/null | wc -l)" -gt 0 ]; then
echo "Downloading MaxMind GeoLite2-City database..."
DOWNLOAD_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
# Use Node.js for download (BusyBox wget leaks auth header on redirects)
if node -e "
const https = require('https');
const fs = require('fs');
setTimeout(() => { console.error('GeoIP download timed out after 60s'); process.exit(1); }, 60000).unref();
const auth = Buffer.from('$MAXMIND_ACCOUNT_ID:$MAXMIND_LICENSE_KEY').toString('base64');
const get = (url, cb) => https.get(url, { headers: url.includes('maxmind.com') ? { Authorization: 'Basic ' + auth } : {} }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) return get(res.headers.location, cb);
if (res.statusCode !== 200) { cb(new Error('HTTP ' + res.statusCode)); return; }
const out = fs.createWriteStream('/tmp/geolite2.tar.gz');
res.pipe(out);
out.on('close', () => cb(null));
}).on('error', cb);
get('$DOWNLOAD_URL', (err) => { if (err) { console.error(err.message); process.exit(1); } else { process.exit(0); } });
"; then
tar -xzf /tmp/geolite2.tar.gz -C /tmp/ 2>/dev/null
MMDB_FILE=$(find /tmp -name 'GeoLite2-City.mmdb' -type f 2>/dev/null | head -1)
if [ -n "$MMDB_FILE" ]; then
mv "$MMDB_FILE" "$GEOIP_DB"
echo "GeoLite2-City database updated."
else
echo "WARNING: Downloaded archive but no .mmdb file found (non-fatal)"
fi
rm -rf /tmp/geolite2.tar.gz /tmp/GeoLite2-City_* 2>/dev/null
else
echo "WARNING: Failed to download GeoLite2 database (non-fatal — geo features disabled)"
fi
else
echo "GeoLite2-City database is up to date."
fi
else
echo "MaxMind credentials not set — GeoIP lookup disabled."
fi
# If running production mode (node dist/server.js) and dist is stale, recompile
if [ -f "src/server.ts" ] && echo "$@" | grep -q "npm.*start\|node.*dist"; then
if [ ! -f "dist/server.js" ] || [ "src/server.ts" -nt "dist/server.js" ]; then
echo "Compiling TypeScript (dist/ is missing or stale)..."
npx tsc 2>&1 || echo "WARNING: TypeScript compilation had errors"
echo "Compilation complete."
fi
fi
echo "Starting server..."
# Drop to node user if running as root (production image uses su-exec)
if [ "$(id -u)" = "0" ] && command -v su-exec >/dev/null 2>&1; then
exec su-exec node "$@"
else
exec "$@"
fi