Files
webmail-client/docker/entrypoint.sh
T
lukasandClaude Opus 5 5c59eecff2 Deploy: make the database wait say why it is failing
On a Synology first run, migrate exited with "postgres:5432 never accepted a
connection" after the full 180s — and that was the entire diagnostic output,
despite the network being demonstrably fine afterwards (postgres resolved and
port 5432 was open from a throwaway container on the same network).

The retry loop caught OSError and discarded it. It now prints the first
failure immediately and repeats every 30s, with the hostname's resolved
address alongside, and includes the last error in the final message. A
DNS failure, a refused connection and a silently dropped packet each produce
a different line instead of the same useless one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
2026-09-14 20:48:29 +02:00

113 lines
4.1 KiB
Bash
Executable File

#!/bin/sh
# One image, several roles. Called as: entrypoint <command> [args...]
#
# api uvicorn, behind the `web` nginx container
# worker-sync lease-owned account sync supervisor (SPEC §6.3)
# worker-jobs arq consumer (interactive + bulk queues)
# worker-all both in one process (small installs; halves RAM use)
# migrate core Alembic upgrade + per-plugin install; exits 0 when done
# reindex rebuild the search index from Postgres
# plugins `python -m app.plugins.cli ...` passthrough
# shell /bin/sh (debugging)
#
# Any other argument is exec'd verbatim, so `docker compose run api python -c ...`
# still works.
set -eu
: "${API_HOST:=0.0.0.0}"
: "${API_PORT:=8000}"
: "${API_WORKERS:=1}"
# Plugins whose migrations are applied on every `migrate` run. Space-separated so
# an operator can add a third-party plugin without editing the image.
: "${ENABLED_PLUGINS:=calendar contacts labels filters}"
cmd="${1:-api}"
[ "$#" -gt 0 ] && shift || true
# Belt and braces on top of the compose healthcheck: a NAS with slow disks can
# take a while over initdb, and `migrate` is a one-shot with no restart policy —
# if it starts a moment early it fails the whole bring-up.
wait_for_db() {
python - <<'PYEOF'
import os, socket, sys, time
from urllib.parse import urlparse
url = os.environ.get("DATABASE_URL", "")
parsed = urlparse(url.replace("+asyncpg", ""))
host = parsed.hostname or "postgres"
port = parsed.port or 5432
deadline = time.time() + float(os.environ.get("DB_WAIT_SECONDS", "180"))
# Report WHY it is not connecting, not just that it isn't: the first failure
# immediately, then every 30s. A silent retry loop that ends in "never accepted"
# tells whoever is installing this precisely nothing.
last_error = None
next_report = 0.0
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=3):
print(f"[entrypoint] database accepting connections at {host}:{port}")
sys.exit(0)
except OSError as exc:
last_error = exc
if time.time() >= next_report:
try:
resolved = socket.gethostbyname(host)
except OSError as dns_exc:
resolved = f"UNRESOLVED ({dns_exc})"
print(
f"[entrypoint] waiting for {host}:{port} "
f"(resolves to {resolved}): {type(exc).__name__}: {exc}",
flush=True,
)
next_report = time.time() + 30
time.sleep(2)
print(
f"[entrypoint] ERROR: {host}:{port} never accepted a connection. "
f"Last error: {type(last_error).__name__}: {last_error}",
file=sys.stderr,
)
sys.exit(1)
PYEOF
}
run_migrations() {
wait_for_db
echo "[entrypoint] core schema: alembic upgrade head"
alembic upgrade head
for plugin in $ENABLED_PLUGINS; do
echo "[entrypoint] plugin schema: $plugin"
python -m app.plugins.cli install "$plugin"
done
echo "[entrypoint] migrations complete"
}
case "$cmd" in
api)
# --proxy-headers + --forwarded-allow-ips are what let the app see the
# real scheme through nginx/DSM. Without them every request looks like
# plain http and the session cookie loses its __Host- prefix and Secure
# flag (app/identity/cookies.py:22). The allow-list is the internal
# docker network only; this port is never published directly.
exec uvicorn app.main:create_app \
--factory \
--host "$API_HOST" \
--port "$API_PORT" \
--workers "$API_WORKERS" \
--proxy-headers \
--forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \
--no-server-header \
"$@"
;;
worker-sync) WORKER_ROLE=sync exec python -m workers.run "$@" ;;
worker-jobs) WORKER_ROLE=jobs exec python -m workers.run "$@" ;;
worker-all) WORKER_ROLE=all exec python -m workers.run "$@" ;;
migrate) run_migrations ;;
reindex) exec python scripts/reindex_search.py "$@" ;;
plugins) exec python -m app.plugins.cli "$@" ;;
shell) exec /bin/sh "$@" ;;
*) exec "$cmd" "$@" ;;
esac