Docker
Images, containers, Dockerfile, volumes, networking.
Theory
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
# 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"]
# 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
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 —
latestbreaks 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
.dockerignoreto exclude node_modules, .git, and build artifacts — shrinks context and speeds builds. - Set explicit
HEALTHCHECKor 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
- Baking secrets into images with
ENV API_KEY=...— layers are readable forever; use runtime secrets injection. - Running
docker runwithout--restart unless-stoppedin 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
Practical Exercises
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.
Create docker-compose.yml with app + Postgres + Redis. Use named volumes for Postgres data. Verify data survives docker compose down && docker compose up.
Start a container with a bad CMD. Use docker logs, docker inspect, and docker exec to diagnose. Fix the Dockerfile and rebuild.