diff --git a/Dockerfile b/Dockerfile index 77d8a5c..0df1227 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/backend/docker_entrypoint.sh b/backend/docker_entrypoint.sh index e3314f5..0f9221c 100644 --- a/backend/docker_entrypoint.sh +++ b/backend/docker_entrypoint.sh @@ -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)..."