# syntax=docker/dockerfile:1.7 FROM python:3.13-slim AS builder ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_NO_CACHE_DIR=1 \ PYTHONDONTWRITEBYTECODE=1 WORKDIR /build COPY pyproject.toml requirements.lock ./ COPY app ./app # requirements.lock pins every transitive dependency to the known-good # versions captured by `pip freeze` against a clean install. Install # from it first, then add the project itself with --no-deps so the # lockfile is the single source of truth and pyproject's range pins # (>=) can't drift on rebuild. RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --upgrade pip \ && /opt/venv/bin/pip install -r requirements.lock \ && /opt/venv/bin/pip install --no-deps . FROM python:3.13-slim AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PATH="/opt/venv/bin:$PATH" \ TZ=UTC RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /opt/venv /opt/venv WORKDIR /app COPY app ./app COPY alembic ./alembic COPY alembic.ini ./ # Default command is the web app; scheduler container overrides via `command:`. EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] # --------------------------------------------------------------------------- # Test stage — same Python, same prod deps, plus dev extras (pytest + # aiosqlite). Built and run only via docker-compose.test.yml; never shipped. # --------------------------------------------------------------------------- FROM python:3.13-slim AS test ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PATH="/opt/venv/bin:$PATH" \ TZ=UTC \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_NO_CACHE_DIR=1 COPY --from=builder /opt/venv /opt/venv WORKDIR /app COPY pyproject.toml requirements.lock ./ COPY app ./app COPY alembic ./alembic COPY alembic.ini ./ # tests/ is excluded by .dockerignore (prod-correct: never bake tests into # a shipped image). docker-compose.test.yml bind-mounts ./tests:/app/tests # at run time, so the suite is always available without baking it in. # The lockfile already contains the dev extras (pytest, ruff, aiosqlite, # ...) because it was generated against a test-stage install. Same # install pattern as the builder stage: lockfile first, project --no-deps. RUN /opt/venv/bin/pip install -r requirements.lock \ && /opt/venv/bin/pip install --no-deps . CMD ["pytest", "tests/", "-v"]