Objective
Build a small, reproducible, non-root image for a Python web service. Use multi-stage, BuildKit cache mounts for pip, and a distroless or slim runtime image.
Tasks
Task 1: Create the project
mkdir -p ~/multi-stage-lab && cd ~/multi-stage-lab
cat > app.py <<'EOF'
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type','text/plain')
self.end_headers()
self.wfile.write(b'ok')
HTTPServer(('0.0.0.0', 8080), H).serve_forever()
EOF
cat > requirements.txt <<'EOF'
EOF
Task 2: Author the Dockerfile
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS build
WORKDIR /src
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --user --no-cache-dir --upgrade pip && \
pip install --user --no-cache-dir httpie 2>/dev/null || true
COPY app.py ./
FROM gcr.io/distroless/python3-debian12:nonroot
WORKDIR /app
COPY --from=build /root/.local /root/.local
COPY --from=build /src/app.py ./
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["python3", "/app/app.py"]
Task 3: Build and inspect
DOCKER_BUILDKIT=1 docker build -t myorg/handson:1.0.0 .
docker image ls myorg/handson:1.0.0
docker history myorg/handson:1.0.0 --no-trunc
Note the size: a distroless/python3 runtime image with only
your app is typically ~100 MB; a full python:3.12 runtime would
be ~1 GB.
Task 4: Run and verify
docker run --rm -d --name hands-on -p 8080:8080 myorg/handson:1.0.0
sleep 1
curl -s http://localhost:8080
# ok
docker exec hands-on id
# uid=65532(nonroot) gid=65532(nonroot) groups=65532(nonroot)
docker rm -f hands-on
Task 5: Reproducibility check
Run the build again with no source changes. The image digest should be identical:
docker image inspect myorg/handson:1.0.0 --format '{{.Id}}'
DOCKER_BUILDKIT=1 docker build -t myorg/handson:1.0.0 . # no-op
docker image inspect myorg/handson:1.0.0 --format '{{.Id}}'
Cleanup
docker rmi myorg/handson:1.0.0