# 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, 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 ./ # Copy the full source tree, then build the wheel once. We deliberately # avoid the "stub __init__.py, build wheel, then rebuild" pattern — it # left stale `__init__.py` content in the wheel because pip wheel reuses # the cached wheel metadata when the name+version matches. See git # history on this file for the long version. 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[sqlcipher]' \ && rm -rf /wheels # NOTE: we deliberately do NOT drop privileges to the `cyclone` user. # Named volumes mount as root inside the container, and chown-ing them # requires CAP_CHOWN (root). The standard hardened pattern is an # entrypoint script that chowns as root then drops to the app user via # gosu/su-exec — adds a dependency + an entrypoint file. For v1 we run # as root inside the container; Docker's user-namespace remapping is # the recommended host-level isolation. The `cyclone` user is created # above and survives only so file ownership in bind mounts stays # consistent. To harden later: install gosu + add an entrypoint script # that does `chown -R cyclone:cyclone /var/lib/cyclone/... && exec gosu # 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/health || exit 1 ENTRYPOINT ["tini", "--"] CMD ["python", "-m", "cyclone", "serve"]