Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 2025/efficient-python-dockerfile/.python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13
3 changes: 2 additions & 1 deletion 2025/efficient-python-dockerfile/Dockerfile.06_uv
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM python:3.13-slim-bookworm

RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential && \
build-essential \
curl && \
apt-get clean && rm -rf /var/lib/apt/lists/*

# Download the latest installer, install it and then remove it
Expand Down
162 changes: 162 additions & 0 deletions 2025/efficient-python-dockerfile/Dockerfile.11_final_v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# ---------------------------- Global Build Arguments ----------------------- #
# Base Python version to use
ARG PYTHON_VERSION=3.13.9

# Immutable base image digests ensure reproducible builds.
# You can override these on demand to pull in security‑patched images.
ARG PYTHON_BASE_DIGEST=sha256:f6f069796f24b22e9b5986f34834e60b5aa6fc801094b072720dfd7c28747955
ARG PYTHON_SLIM_DIGEST=sha256:e66df2153a7cc47b4438848efb65e2d9442db4330db9befaee5107fc75464959

# ------------------------------ Builder Stage ------------------------------ #
# This stage builds dependencies and the application environment.
# It contains compilers and build tools that are excluded from the final image.
FROM python:${PYTHON_VERSION}-bookworm@${PYTHON_BASE_DIGEST} AS builder

# Use Bash with pipefail to ensure safer RUN command error handling in this stage
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Build metadata (overridden in CI)
ARG BUILD_DATE="unknown"
ARG GIT_COMMIT="dev"

# Pin critical library versions for determinism
ARG LIBSSL_VER=3.0.17-1~deb12u3
ARG LIBFFI_VER=3.4.4-1
ARG BUILD_ESSENTIAL_VER=12.9
ARG CURL_VER=7.88.1-10+deb12u14
ARG CACERTS_VER=20230311+deb12u1
ARG BASH_VER=5.2.15-2+b9

# Cache mounts speed up rebuilds: apt packages and uv downloads
RUN --mount=type=cache,target=/var/cache/apt-builder \
--mount=type=cache,target=/root/.cache \
apt-get update && \
apt-get install --no-install-recommends -y \
bash=${BASH_VER} \
build-essential=${BUILD_ESSENTIAL_VER} \
libssl-dev=${LIBSSL_VER} \
libffi-dev=${LIBFFI_VER} \
curl=${CURL_VER} \
ca-certificates=${CACERTS_VER} \
&& rm -rf /var/lib/apt/lists/*

# Install UV package manager securely
ADD https://astral.sh/uv/install.sh /install.sh
RUN chmod 755 /install.sh && /install.sh && rm /install.sh
ENV PATH="/root/.local/bin:${PATH}"

# Application source directory
WORKDIR /app

# Copy dependency manifests first for cache optimization
COPY pyproject.toml uv.lock* ./

# Install dependencies into an in‑project virtual environment (.venv)
# using uv; cached mount makes future installs much faster.
RUN --mount=type=cache,target=/root/.cache/uv \
if [ -f uv.lock ]; then \
uv sync --frozen --no-dev; \
else \
uv sync --no-dev; \
fi

# Export accurate dependency list (SBOM-like trace)
RUN ( \
cd .venv/lib && \
find . -type f -path "*/METADATA" ! -path "*/__pycache__/*" | \
while IFS= read -r f; do \
awk \
'{ gsub(/\r/,""); gsub(/\n/,""); gsub(/[[:cntrl:]]/,""); } \
/^[[:space:]]*Name:[[:space:]]*/ {sub(/^[[:space:]]*Name:[[:space:]]*/,""); name=$0} \
/^[[:space:]]*Version:[[:space:]]*/ && $0 !~ /^Metadata-Version:/ {sub(/^[[:space:]]*Version:[[:space:]]*/,""); ver=$0} \
END {gsub(/[[:space:]]+$/,"",name); gsub(/[[:space:]]+$/,"",ver); \
if(length(name)>0 && length(ver)>0) printf "%s==%s\n", name, ver}' \
"$f"; \
done | sort -fu > /app/dependencies.txt \
)

# ---------------------------- Production Stage ----------------------------- #
# The runtime image is minimal: only the app, dependencies, and tiny init.
FROM python:${PYTHON_VERSION}-slim-bookworm@${PYTHON_SLIM_DIGEST} AS production

# Switch back to default POSIX shell for minimal runtime footprint
SHELL ["/bin/sh", "-c"]

# Pin versions for runtime security + install tini for signal reaping
ARG TINI_VER=0.19.0-1+b3
ARG LIBSSL_VER=3.0.17-1~deb12u3
ARG LIBFFI_VER=3.4.4-1

# UID/GID can be overridden for environments that require specific IDs
ARG APP_UID=10000
ARG APP_GID=10000

# Exposed port is also configurable via build args (default 8080)
ARG APP_PORT=8080

# Build metadata
ARG BUILD_DATE="unknown"
ARG GIT_COMMIT="dev"

# Install tini (PID 1 handler) and minimal runtime libs.
# Cache mount improves rebuild speed but isolates per‑stage to avoid lock errors.
RUN --mount=type=cache,target=/var/cache/apt-prod \
apt-get update && \
apt-get install --no-install-recommends -y \
tini=${TINI_VER} \
libssl3=${LIBSSL_VER} \
libffi8=${LIBFFI_VER} \
&& rm -rf /var/lib/apt/lists/*

# Create a non‑root user with predictable UID/GID.
# Many orchestrators (Kubernetes, OpenShift) require numeric IDs.
RUN groupadd -g ${APP_GID} appuser && \
useradd --create-home --shell /usr/sbin/nologin -u ${APP_UID} -g ${APP_GID} appuser

WORKDIR /app

# Application Files
COPY src/ ./src
COPY --from=builder /app/.venv .venv
COPY --from=builder /app/dependencies.txt /app/dependencies.txt

# Copy entry script into image
COPY docker-entrypoint.11_final_v2.sh /app/docker-entrypoint.sh
RUN chmod 755 /app/docker-entrypoint.sh

# Change ownership to non‑root user
RUN chown -R ${APP_UID}:${APP_GID} /app
USER ${APP_UID}:${APP_GID}

# Environment
ENV PATH="/app/.venv/bin:${PATH}" \
PYTHONUNBUFFERED=1 \
PYTHONOPTIMIZE=1 \
PYTHONHASHSEED=0 \
UVICORN_WORKERS=2 \
APP_PORT=${APP_PORT}

# Expose application
EXPOSE ${APP_PORT}

# Remove any setuid/setgid binaries (least‑privilege policy)
RUN chmod u-s,g-s $(find / -perm /6000 2>/dev/null || true) || true

# Readonly best practices at runtime: support for `--read-only` & tmpfs mounts
VOLUME ["/tmp"]

# Metadata
LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$GIT_COMMIT \
org.opencontainers.image.title="FastAPI Test App Container" \
org.opencontainers.image.description="Reproducible FastAPI container with UV, tini, caching, OCI metadata"

# Add route to FastAPI for health check
#HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
# CMD curl -f http://localhost:${APP_PORT}/health || exit 1

# tini becomes PID 1 to reap zombie processes and forward signals correctly
ENTRYPOINT ["/usr/bin/tini", "--"]

# Run through entrypoint to support dynamic env configuration
CMD ["/app/docker-entrypoint.sh"]
81 changes: 80 additions & 1 deletion 2025/efficient-python-dockerfile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,83 @@ docker build --secret id=DB_PASSWORD \
**Running the image**
```
docker run -p 8080:8080 10_final
```
```

### Build `Dockerfile.11_final_v2`

This iteration:

- **Multi‑stage build:** deterministic, layered design separating build and runtime.
- **Reproducible**: base images pinned by digest (`sha256:`) and `uv` dependency locking.
- **Secure**: runs as a non‑root numeric user within a minimal runtime base.
- **Signal‑safe**: managed by `tini` PID 1 and a lightweight shell entrypoint.
- **Configurable**: all runtime options (port, host, workers, secrets) sourced from environment vars.
- **Secretes**: Moved from build time to run time.

#### Build

```bash
docker build --no-cache --target production \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg GIT_COMMIT=$(git rev-parse HEAD) \
-f Dockerfile.11_final_v2 . -t 11_final_v2
```

You can override the pinned digest when you want to update Python security patches:

```bash
docker build \
--no-cache \
--target production \
--build-arg PYTHON_VERSION=3.13.9 \
--build-arg PYTHON_BASE_DIGEST=sha256:<new_bookworm_digest> \
--build-arg PYTHON_SLIM_DIGEST=sha256:<new_slim_digest> \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg GIT_COMMIT=$(git rev-parse HEAD) \
-f Dockerfile.11_final_v2 \
-t 11_final_v2 .
```

#### Running the image

```bash
export DB_PASSWORD="mydbpassword"
export DB_USER="mydbuser"
export DB_NAME="mydbname"
export DB_HOST="mydbhost"
export ACCESS_TOKEN_SECRET_KEY="mysecretkey"

docker run \
-p 8080:8080 \
-e DB_PASSWORD \
-e DB_USER \
-e DB_NAME \
-e DB_HOST \
-e ACCESS_TOKEN_SECRET_KEY \
--security-opt no-new-privileges:true \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev \
--cap-drop=ALL \
11_final_v2
```

**Why these flags matter**

- `--target production` selects the hardened final stage from the multi-stage Dockerfile.
- `--build-arg BUILD_DATE` records the build timestamp for traceability in image metadata.
- `--build-arg GIT_COMMIT` bakes in the source revision so you can tie running containers back to code.
- `--build-arg PYTHON_VERSION`, `PYTHON_BASE_DIGEST`, `PYTHON_SLIM_DIGEST` let you override pinned digests when patching Python, while keeping reproducibility explicit.
- `-e DB_*` and `-e ACCESS_TOKEN_SECRET_KEY` pass secrets at runtime instead of baking them into the image.
- `--security-opt no-new-privileges:true` blocks privilege escalation inside the container.
- `--read-only` mounts the container filesystem as read-only to limit tampering; combine with `--tmpfs /tmp:rw,noexec,nosuid,nodev` to provide a safe scratch space.
- `--cap-drop=ALL` removes Linux capabilities not needed by the app, shrinking the attack surface.

**Inspect Container**

```bash
docker inspect --format '{{json .Config.Labels}}' 11_final_v2
```

```bash
docker run --rm 11_final_v2 bash -c "cat dependencies.txt"
```
38 changes: 38 additions & 0 deletions 2025/efficient-python-dockerfile/docker-entrypoint.11_final_v2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh
# ------------------------------------------------------------------------------
# Docker Entrypoint for the FastAPI + Uvicorn app
# ------------------------------------------------------------------------------
# This script:
# - Reads runtime configuration from environment variables
# - Defaults to safe production values
# - Executes Uvicorn as PID 1 child (so Tini can manage signals cleanly)
# ------------------------------------------------------------------------------

set -e # Exit on any error
set -u # Treat unset variables as errors

# ----------------------------- Runtime Config ----------------------------- #
# Host, Port, and Worker Count can be overridden at runtime:
UVICORN_HOST="${UVICORN_HOST:-0.0.0.0}"
UVICORN_PORT="${APP_PORT:-8080}"
UVICORN_WORKERS="${UVICORN_WORKERS:-2}"
UVICORN_LOG_LEVEL="${UVICORN_LOG_LEVEL:-info}"

# Application entrypoint (FastAPI app path)
APP_IMPORT_PATH="${UVICORN_APP_IMPORT_PATH:-src.main:app}"

# ----------------------------- Logging Startup ---------------------------- #
echo "[Entrypoint] Starting FastAPI app via Uvicorn"
echo "[Entrypoint] Host: ${UVICORN_HOST}"
echo "[Entrypoint] Port: ${UVICORN_PORT}"
echo "[Entrypoint] Workers: ${UVICORN_WORKERS}"
echo "[Entrypoint] Log Level: ${UVICORN_LOG_LEVEL}"
echo "[Entrypoint] App Import Path: ${APP_IMPORT_PATH}"

# ----------------------------- Run Uvicorn ------------------------------- #
# exec replaces the shell with uvicorn so it receives signals directly.
exec uvicorn "${APP_IMPORT_PATH}" \
--host "${UVICORN_HOST}" \
--port "${UVICORN_PORT}" \
--workers "${UVICORN_WORKERS}" \
--log-level "${UVICORN_LOG_LEVEL}"
4 changes: 3 additions & 1 deletion 2025/efficient-python-dockerfile/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description = "A very simple FastAPI application"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.95.0",
"fastapi>=0.121.1",
"uvicorn>=0.21.1",
"h11>=0.16.0,<0.18.0", # explicitly override vulnerable transitive version (GHSA-vqfr-h8mv-ghfj)
"starlette>=0.49.1,<0.51.0" # ← enforce patched version (CVE‑2025‑62727)
]
Loading