Solutions — Dockerfile Design
Lesson 02 · Cloud Infrastructure & DevOps
Solution to Task 1 — Hit a target size (under 80 MB)
The key is switching the runtime stage to Alpine while keeping a proper builder. Alpine’s Python image is much smaller than Debian slim because musl libc is lighter than glibc and Alpine ships no apt infrastructure.
# syntax=docker/dockerfile:1.7
FROM python:3.11-alpine AS builder
# Alpine doesn't ship build tools by default — install them before pip.
RUN apk add --no-cache gcc musl-dev libffi-dev
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.11-alpine
COPY --from=builder /install /usr/local
RUN adduser -D -u 1000 app
WORKDIR /app
COPY --chown=app:app app.py .
USER app
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Expected measurements:
url-api:0.2(slim runtime): ~95 MBurl-api:hw1(alpine runtime): ~65–75 MB
pydantic and some FastAPI deps have C extensions; on musl libc they may not have prebuilt wheels and pip falls back to building from source. apk add gcc musl-dev libffi-dev covers the common cases.Important caveat: Alpine uses musl libc. A small fraction of Python packages (notably some compiled C extensions) ship glibc-only wheels. If you see “no matching distribution” or unexpected
ImportError at runtime, slim is the safer default. For the URL-shortener, Alpine works fine.
Note the difference in adduser syntax: Alpine uses adduser -D -u 1000 app (BusyBox syntax), not Debian’s useradd --create-home --uid 1000 app.
Solution to Task 2 — Use uv instead of pip
Replace the builder’s pip install with uv. First install uv into the builder stage, then use it:
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS builder
# Install uv (fast resolver + installer)
RUN pip install --no-cache-dir uv
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --system --prefix=/install -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /install /usr/local
RUN useradd --create-home --uid 1000 app
WORKDIR /app
COPY --chown=app:app app.py .
USER app
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Typical timings for the URL-shortener with its 5 dependencies (your hardware will vary):
- pip, no-cache: ~30 s
- uv, no-cache: ~4 s — about 7× faster
Final image size is identical to the pip version; uv just resolves and downloads the same wheels faster.
--system. Without it, uv detects that it’s not inside an active virtual environment and refuses to install into the system Python. The error message is: “uv requires a virtual environment to install packages”. Add --system to override.
Solution to Task 3 — Pin everything by digest
After running docker inspect python:3.11-slim --format '{{index .RepoDigests 0}}' you get a digest like python@sha256:abc123.... Use that in both FROM lines:
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim@sha256:abc123...your-actual-digest... AS builder
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.11-slim@sha256:abc123...your-actual-digest...
COPY --from=builder /install /usr/local
RUN useradd --create-home --uid 1000 app
WORKDIR /app
COPY --chown=app:app app.py .
USER app
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
(a) Why pinning by digest matters:
Pinning by digest means the same Dockerfile always pulls the exact same bytes, regardless of when or where you build it. A tag like python:3.11-slim is a mutable pointer that the registry can repoint at any time — security patches, breaking minor-version updates, even (rarely) malicious replacements via typosquatting or registry compromise. A digest is content-addressed: if the bytes change, the digest changes, and your build either pulls identical bytes or fails loudly with a “manifest unknown” error. You cannot silently get a different image.
(b) Keeping the pinned digest fresh:
A bot like Dependabot or Renovate watches for new digests published under the same tag (python:3.11-slim) and automatically opens a pull request bumping the digest in your Dockerfile. Your CI pipeline rebuilds the image against the new digest, runs tests, and merges if green. Separately, docker scout cves <image> (or Trivy in Lesson 05) flags new CVEs in the currently-pinned digest, giving you a signal to rotate it proactively rather than reactively. The combination gives you “always current, never surprised” base images.
Closing thought: Lesson 03 stops thinking about a single image and starts thinking about a system — api + worker + Postgres + Redis, all wired together with docker-compose.