| name | containers |
| description | Author Dockerfiles, docker-compose stacks, and Kubernetes manifests as text artifacts: multi-stage builds, layer caching, image-size discipline, service composition with health checks, Deployment/Service/ConfigMap manifests, and pure-Python verification of the resulting YAML/Dockerfile. |
| metadata | {"dependencies":["dockerfile-parse","PyYAML"]} |
Containers Recipe
Tasks in this skill produce text artifacts (Dockerfile, docker-compose.yml,
k8s/*.yaml) that downstream tests parse and assert on. No Docker daemon is
required at validation time. Work top-to-bottom; each step is an independent
unit you can lift verbatim.
Step 1 — Write a minimal Dockerfile for a Python service
Pin the base image to a digest-stable tag, set a working directory, install
system deps before app deps, and expose the entrypoint. Order matters: stable
layers first, source last, so cache only invalidates on the line that changed.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
EXPOSE 8000
CMD ["python", "-m", "src.main"]
Step 2 — Convert it to a multi-stage build
A builder stage compiles wheels; a slim runtime stage copies only the
installed site-packages and the app. The runtime image carries no compiler,
no build cache, and no requirements.txt.
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.11-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels
COPY src/ ./src/
USER 1001
EXPOSE 8000
CMD ["python", "-m", "src.main"]
Add a .dockerignore so the build context excludes the things tests should
not see leaked into layers:
.git
.venv
__pycache__
*.pyc
.env*
tests/
Step 3 — Parameterize via build args and runtime env
Build args are baked at build time (ARG); env vars are read at container
start (ENV). Use ARG for things that change between build targets
(version, base tag) and ENV for runtime configuration the app reads.
ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim
ARG APP_VERSION=0.0.0
ENV APP_VERSION=${APP_VERSION} \
APP_PORT=8000 \
PYTHONUNBUFFERED=1
LABEL org.opencontainers.image.version=${APP_VERSION}
EXPOSE ${APP_PORT}
Step 4 — Compose multiple services with health checks
docker-compose.yml declares services, a shared network (implicit by
default), named volumes, and health checks. depends_on with
condition: service_healthy only works if the dependency has a working
healthcheck — otherwise it merely sequences startup, not readiness.
version: "3.9"
services:
api:
build: { context: ., target: runtime }
ports: ["8000:8000"]
environment:
DATABASE_URL: postgresql://app:app@db:5432/app
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys;sys.exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status==200 else 1)"]
interval: 10s
timeout: 3s
retries: 5
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
Notes that bite: the healthcheck command must exist inside the image
(curl is absent from alpine; use wget or a python one-liner). Named
volumes require a top-level volumes: declaration.
Step 5 — Write a Kubernetes Deployment with probes
A Deployment manages a ReplicaSet of Pods. Selectors must match Pod
template labels exactly — a mismatch silently produces zero endpoints.
Liveness vs readiness: liveness restarts the container on failure;
readiness gates traffic without restarting.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels: { app: api }
spec:
replicas: 2
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: registry.example.com/api:0.1.0
ports:
- containerPort: 8000
envFrom:
- configMapRef: { name: api-config }
readinessProbe:
httpGet: { path: /healthz, port: 8000 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /healthz, port: 8000 }
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "512Mi" }
Step 6 — Add a Service and a ConfigMap
The Service selector must match the Pod labels from Step 5. targetPort
points at the container's containerPort; port is what other cluster
clients connect to. Keep ConfigMap keys as flat strings — the values land
as env vars when referenced via envFrom.
apiVersion: v1
kind: Service
metadata: { name: api }
spec:
selector: { app: api }
ports:
- name: http
port: 80
targetPort: 8000
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata: { name: api-config }
data:
LOG_LEVEL: "INFO"
FEATURE_FLAG_X: "true"
DATABASE_URL: "postgresql://app:app@db:5432/app"
Step 7 — Verify artifacts in pure Python (no daemon)
Tests parse the produced files and assert on structure. Use
dockerfile-parse for Dockerfiles and yaml.safe_load_all for compose and
multi-document Kubernetes manifests.
from dockerfile_parse import DockerfileParser
import yaml, pathlib
def check_dockerfile(path):
dfp = DockerfileParser(fileobj=open(path))
stages = [s for s in dfp.structure if s["instruction"] == "FROM"]
assert len(stages) >= 2, "expected multi-stage build"
users = [s for s in dfp.structure if s["instruction"] == "USER"]
assert users, "runtime stage must drop root via USER"
assert any(s["instruction"] == "EXPOSE" for s in dfp.structure)
def check_compose(path):
doc = yaml.safe_load(open(path))
assert "services" in doc and doc["services"], "no services declared"
for name, svc in doc["services"].items():
if "depends_on" in svc:
for dep, cfg in (svc["depends_on"] or {}).items():
if isinstance(cfg, dict) and cfg.get("condition") == "service_healthy":
assert "healthcheck" in doc["services"][dep], \
f"{dep} needs healthcheck for service_healthy condition"
def check_k8s(path):
docs = [d for d in yaml.safe_load_all(open(path)) if d]
by_kind = {d["kind"]: d for d in docs}
dep = by_kind["Deployment"]
svc = by_kind["Service"]
pod_labels = dep["spec"]["template"]["metadata"]["labels"]
assert svc["spec"]["selector"].items() <= pod_labels.items(), \
"Service selector must match Pod labels"
container = dep["spec"]["template"]["spec"]["containers"][0]
cport = container["ports"][0]["containerPort"]
tport = svc["spec"]["ports"][0]["targetPort"]
assert cport == tport, "Service targetPort must match containerPort"
Step 8 — Self-check before handing in
Run through this list against the artifacts on disk:
- Dockerfile: at least two
FROM stages; final stage runs as non-root
(USER directive); COPY of source comes after dependency installation;
no pip install cache leaked (--no-cache-dir or explicit cleanup).
- Compose: every
depends_on: service_healthy target has a healthcheck;
all named volumes appear in the top-level volumes: block; the
healthcheck command exists inside the referenced base image.
- Kubernetes:
Service.spec.selector is a subset of
Deployment.spec.template.metadata.labels; targetPort == containerPort;
every configMapRef/secretRef resolves to a manifest in the same
bundle; both probes point at a path the container actually serves;
resources.requests is set so the pod can be scheduled.
Common Failures
- Copying source before installing requirements — every code edit busts the
dependency-install cache.
- Healthcheck command absent from the image (e.g.
curl on alpine):
the service is permanently unhealthy and dependents never start.
- Service selector typo: zero endpoints, traffic times out, no error log.
- Liveness probe with too-short
initialDelaySeconds: container restarts
before it finishes booting, crashloop.
- Mixing build-time
ARG with runtime ENV: secrets baked into a layer
and visible via docker history.