Real production mistakes that cost money, security, and reliability — and the concrete fixes for each. Learn the patterns once; avoid them forever.
1. Why Study Anti-Patterns?
Anti-patterns are tempting paths that feel fine at first but create serious problems at scale. Container anti-patterns are especially dangerous because they work perfectly in development and only bite you in production — under load, after a security incident, or during an on-call outage at 2 AM.
2–10. The Anti-Patterns
The Monolith Container
Cramming an app server, worker, scheduler, and sidecar into a single container makes it impossible to scale components independently, complicates health checks, and breaks the "one process per container" principle. When the scheduler crashes it takes your web server with it.
# Dockerfile starts nginx, worker, and cron CMD ["/entrypoint.sh"] # entrypoint.sh: # nginx & # python worker.py & # crond -f
# Separate images; compose them # docker-compose.yml services: web: image: myapp-nginx worker: image: myapp-worker cron: image: myapp-cron
The Fat Image
Using a full OS image (ubuntu:latest, python:3.12) as your final base ships gigabytes of build tools, shells, and package managers that have no runtime purpose — they just increase attack surface and push time. Every deploy pulls the weight.
FROM python:3.12 COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"] # Result: ~1.1 GB image
FROM python:3.12-slim AS build RUN pip install --prefix=/install -r requirements.txt FROM gcr.io/distroless/python3 COPY --from=build /install /usr/local COPY app.py . CMD ["app.py"] # Result: ~60 MB
The "Latest" Tag in Production
latest is a mutable pointer — the same tag can reference a completely different image after a push. Deployments become non-reproducible: you cannot roll back to "latest" because you no longer know what it was. CI passes; production pulls a new image; everything breaks.
FROM node:latest # Also in k8s manifests: image: myapp:latest imagePullPolicy: Always
FROM node:20.14.0-alpine3.20@sha256:abc123… # In k8s manifests: image: myapp:v2.4.1 # Or pin by digest: image: myapp@sha256:def456…
Container as VM (SSH + Runtime Changes)
SSH-ing into running containers to install packages or tweak configs creates invisible, unreproducible state. The next deployment wipes those changes. Worse, an SSH daemon is a persistent attack surface inside your container network.
# In Dockerfile RUN apt-get install -y openssh-server # "Just SSH in and apt-get install curl" # — every ops engineer ever
# Put the change in the Dockerfile, rebuild, # redeploy. For debugging use: docker exec -it <id> /bin/sh # or kubectl exec — no SSH daemon needed.
Root Everything
Containers run as root by default. If an attacker escapes the container (CVE or misconfigured mount) they land as root on the host. Most apps need zero root privileges at runtime — this is the easiest fix with the highest security impact.
FROM ubuntu:22.04 COPY app /app CMD ["/app"] # Runs as root (UID 0) — default
FROM ubuntu:22.04 RUN useradd -r -u 10001 appuser COPY --chown=appuser app /app USER appuser CMD ["/app"] # Runs as UID 10001
Secrets in Images
Secrets baked into images via ENV, ARG, or COPY are readable by anyone who pulls the image — including via docker history. Even "deleted" secrets remain in earlier layers. Images end up in registries, CI logs, and developer laptops.
ENV DATABASE_PASSWORD=supersecret ARG API_KEY=abc123 # visible in history COPY .env /app/.env # baked into layer
# Build-time: BuildKit secret mount
RUN --mount=type=secret,id=npmrc \
npm install
# Runtime: inject via orchestrator
env:
- name: DB_PASS
valueFrom: { secretKeyRef: … }No Resource Limits
Without CPU and memory limits a single runaway container can starve every other container on the host — an OOM event or a CPU spike becomes a host-wide outage. Kubernetes will also refuse to schedule Pods without requests/limits in many configurations.
docker run myapp # No --memory, no --cpus # One memory leak → host OOM killer fires
docker run --memory=512m --cpus=0.5 myapp
# Kubernetes:
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }Logging to Files
Logs written to files inside the container are lost when the container is removed, fill the writable layer causing disk pressure, and are invisible to docker logs and every log aggregator that relies on stdout/stderr. The twelve-factor app rule: treat logs as event streams.
# app writes to /var/log/app.log # Container dies → logs gone forever # Disk fills → OOM or I/O stall
# Write to stdout/stderr CMD ["node", "server.js"] # Collector (Fluentd/Vector) tails # /var/lib/docker/containers/…/*.log # and ships to your SIEM / Loki / ES
Ignoring Health Checks
Without a HEALTHCHECK instruction the orchestrator assumes the container is healthy the moment the process starts — even if it is stuck in an init loop, deadlocked, or serving 500 errors. Traffic gets routed to broken instances until a human notices.
FROM node:20-alpine COPY . . CMD ["node", "server.js"] # No HEALTHCHECK — orchestrator is blind
HEALTHCHECK --interval=15s \
--timeout=3s \
--retries=3 \
CMD curl -f http://localhost:8080/health \
|| exit 111. The Container Maturity Model
Where does your team sit today? Use this model to plan incremental improvements — you don't need to reach Level 3 overnight, but you should know your gaps.
Most teams ship at Level 1. Best-practice hygiene lifts you to Level 2. Automated gates and policy-as-code deliver Level 3.
Quizzes
Quiz 1 — Identify the Anti-Pattern
A team's container works in CI but randomly serves stale code in production after a third-party library push. Which anti-pattern is the root cause?
Quiz 2 — What's Wrong with This Dockerfile?
FROM python:3.11
ARG SECRET_KEY=abc123
ENV APP_SECRET=$SECRET_KEY
COPY . /app
CMD ["python", "/app/main.py"]
Quiz 3 — Pick the Right Fix
Your API container deadlocks silently 30 minutes after startup. Kubernetes keeps sending traffic to it for 10 minutes before an engineer notices. Which fix prevents this?
Hands-On Task: Dockerfile Audit
🔧 Audit and Fix the Dockerfile Below
This Dockerfile has six anti-patterns. Identify all of them, then write a corrected version.
FROM ubuntu:latest
ARG DB_PASS=hunter2
ENV DATABASE_PASSWORD=$DB_PASS
RUN apt-get update && apt-get install -y \
python3 python3-pip curl openssh-server
COPY requirements.txt /app/
RUN pip3 install -r /app/requirements.txt
COPY . /app/
WORKDIR /app
CMD ["python3", "app.py"]
- List each anti-pattern with a one-sentence explanation.
- Write a corrected multi-stage Dockerfile that fixes all six.
- Verify with
docker buildanddocker inspectthat no process runs as root.
Show Hint (anti-patterns list)
- latest tag —
FROM ubuntu:latestis unpinned - Secret in image —
ARG DB_PASS/ENV DATABASE_PASSWORD - SSH daemon — container-as-VM, unnecessary attack surface
- Fat image — full Ubuntu + pip; use slim/distroless
- Root user — no
USERinstruction; runs as UID 0 - No HEALTHCHECK — orchestrator stays blind to failures
OPA / Gatekeeper (Kubernetes) — policy-as-code admission controller. Policies like "no :latest", "no root containers", "image must come from internal registry" are enforced at deploy time — the API server rejects non-compliant manifests before they ever run.
CI Gates — tools like Hadolint (Dockerfile linting), Trivy (vulnerability scan), and Conftest (OPA policies against manifests) run in CI pipelines and fail the build before an image is ever pushed. A developer sees the violation in their PR, not in a 3 AM page.
Image signing + admission — Cosign + Sigstore or Notary v2 sign every image at build time; an admission controller rejects unsigned images in production. This prevents a developer from bypassing the pipeline by pushing directly to the registry.
Key Takeaways
- One process per container — independent scaling, isolated failures, clear health semantics
- Multi-stage + minimal base — smaller attack surface, faster deploys, lower registry costs
- Pin by digest — tags are promises, digests are guarantees
- Immutable images — if you need to change something, rebuild and redeploy; never SSH and mutate
- Non-root always — a single
USERinstruction is the highest-ROI security hardening step - Secrets belong in the runtime — BuildKit mounts at build time, orchestrator secrets at runtime; never in the image
- Resource limits are mandatory — no limits means one bad container can take down the entire host
- stdout/stderr for logs — containers are ephemeral; log files inside them are too
- HEALTHCHECK every service — if the orchestrator can't see it, it can't fix it
- Automate enforcement — documentation gets stale; OPA policies and CI gates don't
Course Complete — Congratulations!
You've reached the end of Containers from Scratch — all 33 lessons.
You now understand containers from the Linux kernel primitives that make them possible, through production hardening, orchestration, networking, security, and — finally — the anti-patterns to actively avoid.
Ship smaller. Run safer. Enforce automatically. You've got this. 🚀