Wheel Offline

This note shows a Python Dockerfile that turns every dependency into a wheel in a builder stage and installs them offline in a slim runtime. An inline script parses pyproject.toml with tomllib, rewrites the fastapi and starlette bounds, and writes a safe requirements file to patch a known vulnerability. The builder then caches all wheels, including the project's own wheel. The runtime installs from that directory with no index access, which removes the need for network during the final stage. The note explains each choice, from PIP_ONLY_BINARY to PYTHONOPTIMIZE and the Python-only healthcheck, and lists the trade-offs. Each section pairs the advantages with the costs, so you can judge whether the extra complexity fits your team.

Dockerfile Content

# syntax=docker/dockerfile:1.7
#
##############################
# builder
##############################
FROM python:3.13-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_ONLY_BINARY=:all: \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Pre-cache manifests
COPY pyproject.toml README.md LICENSE* ./

# Fix HIGH vulnerable issue: CVE-2025-62727 by upgrading starlette and fastapi.
RUN --mount=type=cache,target=/root/.cache/pip python - <<'PY'
import tomllib, pathlib, re
def parse_req(s:str):
    m = re.match(r'^\s*([A-Za-z0-9_.-]+)(\\[[^\\]]+\\])?\s*(.*)$', s)
    if m:
        name, extras, rest = m.group(1), (m.group(2) or ''), (m.group(3) or '')
        return name, extras, rest
    name = re.split(r'[><=~!; ]', s, 1)[0]
    return name, '', s[len(name):]

data = tomllib.loads(pathlib.Path('pyproject.toml').read_text())
deps = data.get('project', {}).get('dependencies', [])
safe = []
present = set()
for d in deps:
    name, extras, rest = parse_req(d)
    norm = name.lower().replace('_','-')
    if norm == 'fastapi':
        safe.append(f'fastapi{extras}>=0.118,<0.121')
    else:
        safe.append(d)
    present.add(norm)
if 'starlette' not in present:
    safe.append('starlette>=0.49.1,<0.50')
pathlib.Path('/requirements.safe.txt').write_text('\n'.join(safe) + '\n')
print('Resolved safe deps:', *safe, sep='\n- ')
PY

# Wheel ALL dependencies from the safe list
RUN --mount=type=cache,target=/root/.cache/pip \
    pip wheel --wheel-dir /wheels -r /requirements.safe.txt

# Build wheel of the project itself
COPY src/ ./src/
RUN --mount=type=cache,target=/root/.cache/pip \
    pip wheel --wheel-dir /wheels .


##############################
# runtime
##############################
FROM python:3.13-slim AS runtime

ARG VERSION=0.1.0
ARG VCS_REF=sha
ARG BUILD_DATE

LABEL org.opencontainers.image.title="python-service-template" \
      org.opencontainers.image.description="Dockerfile contest build" \
      org.opencontainers.image.version=$VERSION \
      org.opencontainers.image.revision=$VCS_REF \
      org.opencontainers.image.created=$BUILD_DATE \
      org.opencontainers.image.licenses="Apache-2.0"

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PYTHONOPTIMIZE=2 \
    HOST=0.0.0.0 \
    PORT=5000 \
    WORKERS=1 \
    APP_VERSION=$VERSION \
    GIT_COMMIT_SHA=$VCS_REF

# Non-root
RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser
WORKDIR /home/appuser

# Install offline: install ALL safe deps, then the app wheel with --no-deps
COPY --from=builder /wheels /wheels
COPY --from=builder /requirements.safe.txt /requirements.safe.txt
RUN pip install --no-index --find-links=/wheels -r /requirements.safe.txt \
    && pip install --no-index --find-links=/wheels --no-deps \
       python-service-template --no-compile \
    && rm -rf /wheels /requirements.safe.txt

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=2s --start-period=10s --retries=3 \
  CMD python -c "import sys, http.client; c=http.client.HTTPConnection('127.0.0.1', int(__import__('os').environ.get('PORT','5000')), timeout=1); c.request('GET','/health'); r=c.getresponse(); sys.exit(0 if r.status==200 else 1)" || exit 1

USER 10001:10001

CMD ["python", "-m", "python_service_template.app"]

Techniques Used

Wheel-based Installation

  • Description: Builds wheels in the builder stage and installs them offline in the runtime
  • Technique:
    • pip wheel to build wheels
    • pip install --no-index --find-links=/wheels to install offline
  • Advantages:
    • No internet needed in the runtime stage
    • Reproducible builds
    • Faster installs from wheels
    • Wheels can be cached
  • Disadvantages:
    • Larger builder stage
    • Wheel files must be managed

Dynamic Requirements Parsing

  • Description: Parses pyproject.toml with an inline Python script
  • Technique: Uses tomllib (Python 3.11+) to parse and generate requirements.safe.txt
  • Advantages:
    • Automatically patches security vulnerabilities
    • No separate requirements file to maintain
    • Flexible version overrides
  • Disadvantages:
    • Complex and hard to debug
    • Depends on the Python version (3.11+ required)
    • Complex logic inside the Dockerfile

PIP_ONLY_BINARY

  • Description: Uses only binary wheels and never compiles from source
  • Technique: ENV PIP_ONLY_BINARY=:all:
  • Advantages:
    • Faster builds
    • No build tools needed at runtime
    • Fewer dependencies
  • Disadvantages:
    • Some packages may not have a wheel
    • Depends on wheel availability

PYTHONOPTIMIZE=2

  • Description: Enables Python optimization level 2
  • Technique: ENV PYTHONOPTIMIZE=2
  • Advantages:
    • Removes docstrings and assertions
    • Optimizes bytecode
    • Reduces size
  • Disadvantages:
    • Loses docstrings (may affect some tools)
    • Assertions are removed (may hide bugs)

Python-based Healthcheck

  • Description: Uses Python built-ins instead of an external tool
  • Technique: python -c "import http.client; ..."
  • Advantages:
    • No extra package needed
    • Always available
  • Disadvantages:
    • Long, hard-to-read command
    • May be slower than curl/wget

--no-compile

  • Description: Does not compile bytecode during installation
  • Technique: pip install --no-compile
  • Advantages:
    • Shorter install time
    • Smaller size (no .pyc files)
  • Disadvantages:
    • Python compiles on the fly (slower on first run)

Overall Advantages

  • Offline installation
  • Reproducible builds
  • Automatic security patching
  • No external tools needed for the healthcheck

Overall Disadvantages

  • Complex logic inside the Dockerfile
  • Depends on wheel availability
  • PYTHONOPTIMIZE can cause side effects

See also: Optimization Techniques · UV Alpine Tini