-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
75 lines (61 loc) · 3.29 KB
/
Dockerfile
File metadata and controls
75 lines (61 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# Multi-stage build: keep `uv` and the build-time toolchain out of the
# runtime image. The builder stage materialises `.venv` from the locked
# dep set; the runtime stage copies only the venv + source onto a fresh
# `python:3.14-slim` base. See SECURITY.md "Container Hardening" for the
# threat model.
#####################################################################
# Builder — has uv, pip, and whatever transitive build deps `uv sync`
# touches. Nothing from this stage ships.
#####################################################################
FROM python:3.14-slim AS builder
# `--python-preference only-system` + `UV_PYTHON_DOWNLOADS=never` forbid
# uv from downloading its own Python interpreter — we want the venv
# linked against the same /usr/local Python the runtime stage carries,
# so the `pyvenv.cfg` symlinks resolve after the COPY --from=builder.
ENV UV_PYTHON_DOWNLOADS=never \
UV_PYTHON_PREFERENCE=only-system
RUN pip install --no-cache-dir uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
#####################################################################
# Runtime — minimal: Python, the materialised venv, app source, and
# a non-root user. No uv, no pip cache, no build tools.
#
# Must match the builder's base image — the venv's `pyvenv.cfg` and
# interpreter symlinks reference /usr/local/bin/python3.14. Switching
# to e.g. python:3.14-alpine here would leave broken symlinks because
# Alpine puts Python at /usr/bin/python3.14.
#####################################################################
FROM python:3.14-slim AS runtime
WORKDIR /app
# Create the non-root user FIRST so the subsequent COPY --chown
# directives can reference it. Doing it this way avoids a separate
# `chown -R app:app /app` walk over the (thousands of files in the)
# venv — ownership is baked into each COPY layer at write time.
RUN groupadd --system app \
&& useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app
# Bring across only the venv (built above) and the application source.
# The builder's pip install + uv binary stay behind in the builder
# layer cache; they never enter the published image.
COPY --from=builder --chown=app:app /app/.venv /app/.venv
COPY --chown=app:app src/ src/
USER app
# Put the venv on PATH so `uvicorn` resolves without `uv run` indirection.
# PYTHONDONTWRITEBYTECODE=1 stops Python from attempting `.pyc` writes under
# `__pycache__/` on cold start — they would EROFS-fail under the read-only
# root FS configured in docker-compose.yml. PYTHONUNBUFFERED=1 keeps
# uvicorn's stdout from being held behind line-buffering when running under
# non-TTY container stdio.
ENV PATH="/app/.venv/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
EXPOSE 8000
# Python's stdlib urllib avoids needing curl in the image. urlopen raises
# on non-2xx / timeout / connection-error, exiting Python non-zero and
# marking the container unhealthy.
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health', timeout=2)" || exit 1
# Direct uvicorn invocation — the venv is on PATH, no `uv run` wrapper
# needed (and uv isn't here to call anyway).
CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]