# syntax=docker/dockerfile:1.7 # --------------------------------------------------------------------------- # Chicken Babies R Us — container image # # Multi-stage build: # builder - install build deps, create a venv, resolve Python requirements # runtime - slim image with only runtime libs + the copied venv + app code # # Phase 0 scope: runs as root and has no HEALTHCHECK directive. Phase 6 # (per docs/ROADMAP.md) introduces a non-root user and a HEALTHCHECK. # --------------------------------------------------------------------------- ARG PYTHON_IMAGE=python:3.12-slim-bookworm # ==== Stage 1: builder ===================================================== FROM ${PYTHON_IMAGE} AS builder # Avoid interactive apt prompts and keep pip quiet + deterministic. ENV DEBIAN_FRONTEND=noninteractive \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONDONTWRITEBYTECODE=1 # Build dependencies: `build-essential` covers any wheel that needs to # compile from source; libmagic headers are required by python-magic. # Keep this list minimal to reduce attack surface of the builder stage. RUN apt-get update \ && apt-get install --no-install-recommends -y \ build-essential \ libmagic1 \ libmagic-dev \ && rm -rf /var/lib/apt/lists/* # Dedicated virtualenv at a stable path so the runtime stage can copy it # verbatim. This keeps the runtime image free of build tooling. RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:${PATH}" WORKDIR /build # Install Python dependencies. Copying only requirements.txt first lets # Docker cache the dependency layer when application code changes. COPY requirements.txt /build/requirements.txt RUN pip install --upgrade pip \ && pip install --no-cache-dir -r requirements.txt # ==== Stage 2: runtime ===================================================== FROM ${PYTHON_IMAGE} AS runtime # Runtime-only libs: python-magic needs libmagic1 at import time. Build # tooling is intentionally NOT installed here. ENV DEBIAN_FRONTEND=noninteractive \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 RUN apt-get update \ && apt-get install --no-install-recommends -y \ libmagic1 \ && rm -rf /var/lib/apt/lists/* # Copy the pre-built virtualenv from the builder stage and put it first # on PATH so `python` and installed console scripts (uvicorn) resolve. COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:${PATH}" WORKDIR /app # Application source. Only the `app/` package is needed at runtime; tests # and docs stay out of the image. COPY app /app/app # Git commit SHA wired in at build time. Declared as an ARG with a safe # default so local builds without --build-arg still succeed; surfaced to # runtime via an ENV so the Settings loader can pick it up. ARG GIT_COMMIT_SHA=unknown ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA} # Phase 6 hardening: create a dedicated non-root user with a stable # UID/GID so host bind mounts (data/, media/) have predictable # ownership. We pin 10001 because values < 1000 collide with Debian # system accounts on some host distros. RUN groupadd -g 10001 app \ && useradd -m -u 10001 -g app app \ && chown -R app:app /app /opt/venv USER app EXPOSE 8080 # Phase 6 healthcheck: hits /healthz over the loopback with stdlib # urllib so we don't have to install curl/wget in the runtime image. # Intervals tuned for a small VM: probe every 30s, give a fresh # container 10s to finish startup before the first probe counts. HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD python -c "import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2); sys.exit(0)" || exit 1 # Run Uvicorn directly. --proxy-headers + --forwarded-allow-ips make # Starlette's ProxyHeadersMiddleware trust X-Forwarded-* only from the # listed peer IPs. The trusted-IP value is env-driven so the image # can be reused across topologies: # - local: defaults to 127.0.0.1 (when running uvicorn on the host) # - docker/compose behind Caddy: set FORWARDED_ALLOW_IPS="*" in .env # because the container's source IP is the bridge gateway, not # 127.0.0.1. Safe because the host only binds 127.0.0.1:8080 so # nothing off-host can reach uvicorn directly. # `sh -c exec` keeps uvicorn as PID 1 so SIGTERM still triggers a # graceful shutdown (exec form was fine before, but we need shell # expansion for ${FORWARDED_ALLOW_IPS}). CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port 8080 --proxy-headers --forwarded-allow-ips \"${FORWARDED_ALLOW_IPS:-127.0.0.1}\""]