diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..28f1615 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,12 @@ +.venv/ +venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +tests/ +docs/prodfiles/ +*.production.txt +.git/ +.github/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..490e60e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,74 @@ +# syntax=docker/dockerfile:1.7 +# +# Cyclone backend — FastAPI on python:3.11-slim-bookworm with sqlcipher. +# +# Two-stage build: +# 1. builder — wheels the package with [sqlcipher] extra into /wheels. +# 2. runtime — slim base, non-root user, tini PID 1, curl-based healthcheck. +# +# `sqlcipher` is preferred but the engine falls back to plain SQLite at +# runtime if the package isn't actually installed (see cyclone.db) — so a +# missing libsqlcipher-dev during build will fail loudly here rather than +# silently downgrading encryption in production. + +# ---------- builder ---------- +FROM python:3.11-slim-bookworm AS builder + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libffi-dev \ + libsqlcipher-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the build manifest first so this layer caches across source edits. +COPY pyproject.toml ./ + +# Stub package so `pip wheel` can resolve the `cyclone` project without the +# source tree yet — required because pyproject.toml uses setuptools and +# references `src/cyclone`. +RUN mkdir -p src/cyclone && touch src/cyclone/__init__.py +RUN pip wheel --no-cache-dir --wheel-dir /wheels '.[sqlcipher]' + +# Now copy the real source and rebuild so the wheel reflects the actual code. +COPY src/ ./src/ +RUN pip wheel --no-cache-dir --wheel-dir /wheels '.[sqlcipher]' + +# ---------- runtime ---------- +FROM python:3.11-slim-bookworm + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libsqlcipher-dev \ + curl \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --create-home --uid 1000 --shell /bin/bash cyclone + +WORKDIR /app + +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir --no-index --find-links /wheels cyclone \ + && rm -rf /wheels + +COPY --chown=cyclone:cyclone src/ /app/src/ + +USER cyclone + +EXPOSE 8000 + +# Container-level healthcheck — the compose service healthcheck is +# effectively a duplicate but the Docker `HEALTHCHECK` directive keeps +# `docker ps` honest without needing compose to be running. +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fs http://localhost:8000/api/healthz || exit 1 + +ENTRYPOINT ["tini", "--"] +CMD ["python", "-m", "cyclone", "serve"]