New independent `admin` service (admin.main:app) on the same image, reusing app.db/app.models read-only. Never runs migrations or the scheduler; issues SELECTs only. - Password-gated (ADMIN_CONSOLE_PASSWORD) with a 12h signed cookie; closed by default when the password is empty. - Bound to 127.0.0.1:8091 (SSH-tunnel access); off the intranet/NPM network. - Pages: overview stats, user list + search, per-user history/payment detail, DB usage (information_schema size + row estimates). - Compose: base `admin` service (+prod DB-host override, test mount); Dockerfile bakes admin/ into runtime + test stages. - Tests: tests/test_admin_console.py (auth, queries, page wiring) — 12 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.5 KiB
Docker
75 lines
2.5 KiB
Docker
# 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 admin ./admin
|
|
COPY alembic ./alembic
|
|
COPY alembic.ini ./
|
|
|
|
# Default command is the web app; the scheduler and admin-console containers
|
|
# override via `command:` (see docker-compose.yml).
|
|
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 admin ./admin
|
|
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"]
|