fix: ensure Postgres driver installs in image & stop uv re-sync at runtime

Worker crash-loop root cause (from container log):
  sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres

Two compounding issues:
1. Dockerfile copied pyproject.toml/uv.lock, ran 'uv sync --frozen',
   then 'COPY backend ./backend' which OVERWROTE those dep files with the
   shipped versions. The two could differ, so every entrypoint 'uv run'
   detected drift and REBUILT/re-synced the project at container runtime
   (seen as repeated 'Building crowdsight-backend...' + 'Uninstalled N /
   Installed 1'), never installing the psycopg Postgres driver that the
   image's own lock actually lists.
2. Result: alembic upgrade head over a postgres DATABASE_URL crashed with
   NoSuchModuleError -> worker crash-loop.

Fix:
- Dockerfile: COPY backend (full source) BEFORE 'uv sync --frozen --no-dev',
  so the installed deps match the shipped pyproject.toml/uv.lock exactly.
- Use 'uv run --frozen' for alembic/gunicorn/worker so nothing re-syncs at
  runtime.
- entrypoint: fail fast with a clear message if DATABASE_URL is postgres
  but psycopg is missing (instead of a confusing alembic traceback).
Verified: entrypoint bash syntax ok; 'uv run --frozen ... import psycopg'
passes; psycopg present in git-tracked uv.lock + pyproject.
This commit is contained in:
Kunthawat Greethong
2026-08-31 18:56:18 +07:00
parent 42208c4f5a
commit 8953b8f066
2 changed files with 20 additions and 7 deletions

View File

@@ -34,15 +34,18 @@ COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
WORKDIR /app
# Install backend deps first (cache-friendly)
COPY backend/pyproject.toml backend/uv.lock ./backend/
RUN cd backend && uv sync --frozen --no-dev
# Copy project source
# Copy project source FIRST so the installed deps match the shipped
# pyproject.toml/uv.lock exactly (avoids `uv run` re-syncing at container
# runtime, which previously rebuilt the project every time the entrypoint
# ran and missed the psycopg Postgres driver -> "NoSuchModuleError:
# sqlalchemy.dialects:postgres").
COPY backend ./backend
COPY locales ./locales
COPY package.json ./
# Install backend deps against the final pyproject.toml/uv.lock
RUN cd backend && uv sync --frozen --no-dev
# Make the migration-runner entrypoint executable
RUN chmod +x /app/backend/docker_entrypoint.sh
@@ -55,7 +58,7 @@ RUN echo 'server {\n listen 8080;\n server_name _;\n root /usr/share/nginx/ht
&& rm -f /etc/nginx/sites-enabled/default
# supervisor: run nginx + gunicorn + worker (migrations already run by entrypoint)
RUN echo '[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:nginx]\ncommand=/usr/sbin/nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:backend]\ncommand=/bin/bash -c "cd /app/backend && uv run gunicorn -w 2 -b 0.0.0.0:5001 --timeout 120 wsgi:app"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:worker]\ncommand=/bin/bash -c "cd /app/backend && uv run python worker.py --poll-interval 5"\nautostart=true\nautorestart=true\nstartsecs=2\nstartretries=5\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/crowdsight.conf
RUN echo '[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:nginx]\ncommand=/usr/sbin/nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:backend]\ncommand=/bin/bash -c "cd /app/backend && uv run --frozen gunicorn -w 2 -b 0.0.0.0:5001 --timeout 120 wsgi:app"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:worker]\ncommand=/bin/bash -c "cd /app/backend && uv run --frozen python worker.py --poll-interval 5"\nautostart=true\nautorestart=true\nstartsecs=2\nstartretries=5\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/crowdsight.conf
EXPOSE 8080 5001

View File

@@ -18,8 +18,18 @@ if [[ -z "${DATABASE_URL:-}" ]]; then
exit 1
fi
# If DATABASE_URL points at Postgres, fail fast with a clear message if the
# psycopg driver is missing (instead of a confusing SQLAlchemy
# NoSuchModuleError inside alembic).
if [[ "$DATABASE_URL" == postgres* ]]; then
if ! uv run --frozen python -c "import psycopg" 2>/dev/null; then
echo "[entrypoint] FATAL: DATABASE_URL is PostgreSQL but the psycopg driver is not installed in this image. Rebuild the image so 'uv sync' installs psycopg[binary]." >&2
exit 1
fi
fi
echo "[entrypoint] Running database migrations (alembic upgrade head)..."
uv run alembic upgrade head
uv run --frozen alembic upgrade head
echo "[entrypoint] Migrations complete."
echo "[entrypoint] Starting supervisord (nginx + backend + worker)..."