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.

The Cost Is Real
Running containers as root contributed to the Capital One breach (2019). Fat images bloat registry bills and slow deploys. The "latest" tag has caused countless broken production deployments. These are not theoretical — they are documented, expensive, real-world failures.

2–10. The Anti-Patterns

1

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.

❌ Bad
# Dockerfile starts nginx, worker, and cron
CMD ["/entrypoint.sh"]
# entrypoint.sh:
# nginx &
# python worker.py &
# crond -f
✅ Good
# Separate images; compose them
# docker-compose.yml
services:
  web:    image: myapp-nginx
  worker: image: myapp-worker
  cron:   image: myapp-cron
2

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.

❌ Bad
FROM python:3.12
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# Result: ~1.1 GB image
✅ Good
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
3

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.

❌ Bad
FROM node:latest
# Also in k8s manifests:
image: myapp:latest
imagePullPolicy: Always
✅ Good
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…
4

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.

❌ Bad
# In Dockerfile
RUN apt-get install -y openssh-server
# "Just SSH in and apt-get install curl"
# — every ops engineer ever
✅ Good
# Put the change in the Dockerfile, rebuild,
# redeploy. For debugging use:
docker exec -it <id> /bin/sh
# or kubectl exec — no SSH daemon needed.
5

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.

❌ Bad
FROM ubuntu:22.04
COPY app /app
CMD ["/app"]
# Runs as root (UID 0) — default
✅ Good
FROM ubuntu:22.04
RUN useradd -r -u 10001 appuser
COPY --chown=appuser app /app
USER appuser
CMD ["/app"]  # Runs as UID 10001
6

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.

❌ Bad
ENV DATABASE_PASSWORD=supersecret
ARG API_KEY=abc123        # visible in history
COPY .env /app/.env       # baked into layer
✅ Good
# Build-time: BuildKit secret mount
RUN --mount=type=secret,id=npmrc \
    npm install
# Runtime: inject via orchestrator
env:
  - name: DB_PASS
    valueFrom: { secretKeyRef: … }
7

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.

❌ Bad
docker run myapp
# No --memory, no --cpus
# One memory leak → host OOM killer fires
✅ Good
docker run --memory=512m --cpus=0.5 myapp
# Kubernetes:
resources:
  requests: { cpu: 250m, memory: 256Mi }
  limits:   { cpu: 500m, memory: 512Mi }
8

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.

❌ Bad
# app writes to /var/log/app.log
# Container dies → logs gone forever
# Disk fills → OOM or I/O stall
✅ Good
# Write to stdout/stderr
CMD ["node", "server.js"]
# Collector (Fluentd/Vector) tails
# /var/lib/docker/containers/…/*.log
# and ships to your SIEM / Loki / ES
9

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.

❌ Bad
FROM node:20-alpine
COPY . .
CMD ["node", "server.js"]
# No HEALTHCHECK — orchestrator is blind
✅ Good
HEALTHCHECK --interval=15s \
            --timeout=3s \
            --retries=3 \
  CMD curl -f http://localhost:8080/health \
      || exit 1

11. 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.

Level 1 — Running Containers Containers run in production. Basic Dockerfiles. No enforcement. "It works!" Level 2 — Best Practices Applied Non-root, pinned images, resource limits, health checks, secrets managed correctly. Level 3 — Automated Enforcement OPA/Gatekeeper, CI policy gates, signed images, SBOM, admission control. MATURITY

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?

  • No resource limits
  • Using :latest tag — mutable, non-reproducible
  • Logging to files
  • Missing health check

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"]
  • The base image tag is pinned — that's wrong
  • CMD should be ENTRYPOINT
  • Secret baked into image via ARG/ENV — visible in docker history
  • COPY should come before ARG

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?

  • Add resource limits
  • Switch to a distroless base image
  • Pin the image tag by digest
  • Add a HEALTHCHECK so Kubernetes marks the Pod unhealthy and stops routing traffic

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"]
  1. List each anti-pattern with a one-sentence explanation.
  2. Write a corrected multi-stage Dockerfile that fixes all six.
  3. Verify with docker build and docker inspect that no process runs as root.
Show Hint (anti-patterns list)
  1. latest tagFROM ubuntu:latest is unpinned
  2. Secret in imageARG DB_PASS / ENV DATABASE_PASSWORD
  3. SSH daemon — container-as-VM, unnecessary attack surface
  4. Fat image — full Ubuntu + pip; use slim/distroless
  5. Root user — no USER instruction; runs as UID 0
  6. No HEALTHCHECK — orchestrator stays blind to failures
🏢 Industry: How Companies Enforce Best Practices

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 USER instruction 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. 🚀