diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..57e796f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.venv +.python-version +.pytest_cache +.mypy_cache +.superpowers +__pycache__ +**/__pycache__ +*.py[cod] +*.egg-info +backups/ +quartermaster.db +quartermaster.db-journal +tests/ +docs/ +CLAUDE.md +.dockerignore +Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..863eafb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.7 +FROM python:3.12-slim-bookworm + +COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /usr/local/bin/ + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PROJECT_ENVIRONMENT=/app/.venv \ + PYTHONUNBUFFERED=1 \ + PATH="/app/.venv/bin:$PATH" + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ +RUN uv sync --no-dev --frozen --no-install-project + +COPY src ./src +COPY alembic ./alembic +COPY alembic.ini ./ +COPY scripts ./scripts +COPY README.md ./ +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh + +RUN uv sync --no-dev --frozen \ + && chmod +x /usr/local/bin/entrypoint.sh \ + && chown -R 1000:1000 /app + +USER 1000:1000 +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3).status == 200 else 1)" + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/README.md b/README.md index bdc6ae4..24bd238 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,33 @@ HTTP access logs appear as `event="http_request"` with `method`, `path`, `month_closed`, `template_entry_updated`, `posting_added`, `posting_deleted`) fire at the matching mutation sites. +## Docker + +A `Dockerfile` at the repo root produces a self-contained image that runs +`alembic upgrade head` (with the pre-upgrade backup hook) then +`uvicorn quartermaster.main:app` as a non-root user (`uid:gid 1000:1000`). +The image `EXPOSE`s port 8000 and declares a `HEALTHCHECK` against +`/healthz`. + +Build and smoke-run locally against a tempfile database: + +```sh +docker build -t quartermaster:dev . + +mkdir -p /tmp/qm-data +docker run --rm -p 8000:8000 \ + -e QUARTERMASTER_DB_URL=sqlite:////data/qm.db \ + -v /tmp/qm-data:/data \ + quartermaster:dev +``` + +Then `curl http://127.0.0.1:8000/healthz` should return +`{"status":"ok"}` with JSON access logs on the container's stdout. + +In production on home-ctr-onyx the bind mount is `/mnt/quartermaster/` +and `QUARTERMASTER_DB_URL` points at the DB inside it; see the +compose file for the full wiring. + ## Tests ```sh diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..a5f6510 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -eu + +cd /app + +alembic upgrade head + +exec uvicorn quartermaster.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-config src/quartermaster/logconfig.json