DevOps · Guide

Docker

Images, containers, Dockerfile, volumes, networking.

— min read DevOps

Theory

Docker packages apps with dependencies — optimize images with multi-stage builds and non-root users.

Docker packages applications and dependencies into images — immutable, layered filesystems built from Dockerfile instructions. Each RUN/COPY creates a cacheable layer; put slow-changing steps (dependency install) before frequently changing source copies to maximize build cache hits.

Containers are runnable instances of images, isolated via Linux namespaces (PID, network, mount, UTS) and constrained by cgroups for CPU and memory. Containers share the host kernel (unlike VMs), start in milliseconds, and use far less overhead per instance.

CMD sets default arguments to the container entrypoint; ENTRYPOINT defines the executable that always runs. Use exec form ["python","app.py"] so PID 1 receives signals directly — shell form runs under /bin/sh -c and can mishandle SIGTERM during shutdown.

COPY transfers files from build context into the image; ADD also fetches URLs and auto-extracts local tar archives — prefer COPY for predictable, auditable builds. A .dockerignore file excludes node_modules, .git, and secrets from the build context, speeding builds and preventing credential leaks.

Multi-stage builds chain multiple FROM stages: compile in a builder image, then COPY --from=builder only the binary or static assets into a minimal runtime (distroless, alpine). This routinely shrinks production images from hundreds of MB to under 30 MB.

Volumes persist data outside the container writable layer — named volumes survive container deletion and are managed by Docker. Bind mounts map host paths for dev hot-reload. Never store production database files only inside a container layer without a volume.

Docker networks: bridge (default, containers get private IPs), host (shares host network stack, no port mapping), none (isolated). On custom bridge networks, Docker's embedded DNS resolves service names to container IPs — essential for multi-container communication.

docker compose (Compose file v3) declares services, networks, volumes, and environment variables declaratively. docker compose up -d is the standard local-dev workflow; production clusters typically use Kubernetes, but Compose remains ideal for integration tests and small deployments.

Architecture Diagram

Dockerfile (build context)
         |
    docker build
         |
    Image (layers + manifest)
         |
    docker run --> Container(s)
         |
    Registry (ECR/GCR/DH)

Examples

dockerfile — multi-stage
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /api ./cmd/api

# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]
bash — docker CLI
# Build and run
docker build -t myapp:1.0 .
docker run -d --name myapp -p 8080:8080 --restart unless-stopped myapp:1.0

# Inspect and debug
docker logs -f myapp
docker exec -it myapp sh
docker stats myapp

# Cleanup
docker system prune -f   # remove dangling images/containers
yaml — docker-compose.yml
services:
  api:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
    depends_on:
      db: { condition: service_healthy }
    networks: [backend]

  db:
    image: postgres:16-alpine
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      retries: 5
    networks: [backend]

volumes:
  pgdata:

networks:
  backend:

Interview Questions

What is the difference between CMD and ENTRYPOINT?

ENTRYPOINT is the main executable; CMD supplies default arguments. Combining both: ENTRYPOINT ["python"] + CMD ["app.py"] lets users override args at runtime. Only one ENTRYPOINT per image — later ones override earlier ones.

Why use multi-stage Docker builds?

Builder stages can include compilers, headers, and dev dependencies; the final stage copies only artifacts. Smaller images mean faster deploys, smaller attack surface, and quicker vulnerability scans.

What happens to container data when the container is removed?

Data in the container writable layer is deleted. Named volumes and bind mounts persist. Production databases must use volumes or external managed storage — never rely on container filesystem alone.

Explain Docker layer caching and how to optimize builds.

Each Dockerfile instruction creates a layer; unchanged layers reuse cache. Put COPY package.json + RUN npm install before COPY . so dependency layers cache across code changes. Use --mount=type=cache for BuildKit cache mounts in CI.

What is the difference between docker run and docker compose?

docker run starts a single container with CLI flags — fine for one-offs. docker compose reads a YAML file defining multiple services, networks, and volumes — reproducible across developers and CI. Compose can build images and wire service dependencies.

How do you pass secrets to containers safely?

Never bake secrets into images or commit them in Dockerfiles. Use runtime env vars from orchestrators (K8s Secrets), Docker secrets (Swarm), or external secret managers. Mount read-only secret files with tmpfs where possible.

FAANG: How do you debug a container that exits immediately?

docker logs container_id for stdout/stderr. docker inspect --format "{{.State.ExitCode}}" for exit code. Run interactively: docker run -it --entrypoint sh image to bypass CMD. Check if PID 1 process crashes on missing env vars or failed health dependency.

What is the difference between COPY and ADD?

COPY only copies local build-context files — preferred default. ADD can fetch remote URLs and auto-extract tar archives, which is surprising behavior. Use COPY unless you explicitly need ADD's tar extraction.

Best Practices

  • Pin base image tags to digests or specific versions — latest breaks reproducible builds overnight.
  • Use multi-stage builds: compile in a builder stage, copy only the binary into a slim runtime image.
  • Run containers as non-root (USER app) — root in a container is still root if the kernel is escaped.
  • Add a .dockerignore to exclude node_modules, .git, and build artifacts — shrinks context and speeds builds.
  • Set explicit HEALTHCHECK or use orchestrator probes — a running container is not necessarily a healthy app.
  • One process per container when possible — simplifies logs, signals, and horizontal scaling.

Common Mistakes

Watch for these patterns — they cause most production incidents around Docker.
  • Baking secrets into images with ENV API_KEY=... — layers are readable forever; use runtime secrets injection.
  • Running docker run without --restart unless-stopped in production — containers stay down after host reboot.
  • Using COPY . . before installing dependencies — any source change invalidates the dependency cache layer.
  • Storing data only inside the container filesystem — data is lost on recreate; use named volumes or bind mounts.
  • Ignoring image size — 2GB images slow deploys and increase attack surface; audit with docker history.

Cheat Sheet

FROMBase image for build stage
RUNExecute command during image build
COPYCopy files from build context into image
CMD / ENTRYPOINTDefault command vs fixed entrypoint
EXPOSEDocument which port the container listens on
.dockerignoreExclude files from build context (like .gitignore)
docker build -tBuild image and tag it
docker run -d -pDetached container with port mapping
docker logs -fStream container stdout/stderr
docker exec -itInteractive shell inside running container
docker system pruneRemove unused images, containers, networks
Multi-stage buildSeparate build and runtime images for smaller deploys

Practical Exercises

Multi-stage Node build

Write a Dockerfile with a node:20 builder stage (npm ci && npm run build) and a node:20-alpine runtime copying only dist/ and package.json. Compare image sizes with docker images.

Compose local stack

Create docker-compose.yml with app + Postgres + Redis. Use named volumes for Postgres data. Verify data survives docker compose down && docker compose up.

Debug a failing container

Start a container with a bad CMD. Use docker logs, docker inspect, and docker exec to diagnose. Fix the Dockerfile and rebuild.