contentintech
Learn/devops/Docker
Intermediate~20 min read

Docker

Images versus containers, Dockerfile instructions, layer caching, multi-stage builds, the docker CLI, volumes, networks, Docker Compose, .dockerignore, image size optimization, and registries.

ImagesContainersDockerfileCompose

Docker packages an application together with its dependencies into a portable image that runs identically on a laptop, a CI runner, or a production server. It solves the classic "works on my machine" problem by making the runtime environment part of the artifact you ship. This guide covers the mental model, writing efficient Dockerfiles, the day-to-day CLI, persistence and networking, Compose, and slimming your images.

Images vs Containers

An image is a read-only template built from layers. A container is a running (or stopped) instance of an image with a thin writable layer on top. One image can spawn many containers, just as one class can produce many objects.

ConceptNatureAnalogy
ImageImmutable, layered templateA class / blueprint
ContainerRunning instance with writable layerAn object / instance
RegistryStore for named, versioned imagesA package repository

Containers are ephemeral

Anything written inside a container's writable layer is lost when the container is removed. Persist real data in volumes, and treat containers as disposable, replaceable units.

Dockerfile Instructions

A Dockerfile is a recipe. Each instruction that changes the filesystem produces a new layer. The most common instructions:

InstructionPurpose
FROMBase image to build on
WORKDIRSet working directory for later steps
COPY / ADDCopy files into the image
RUNExecute a command at build time
ENVSet environment variables
EXPOSEDocument the listening port
CMD / ENTRYPOINTDefault command when the container starts
dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app

# Copy manifests first so npm install is cached across code changes
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Now copy source (changes here won't bust the install layer)
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Layer Caching

Docker caches each layer and reuses it if the instruction and its inputs are unchanged. The moment one layer's input changes, every layer after it is rebuilt. The golden rule: order instructions from least to most frequently changing. Dependency manifests change rarely, so copy and install them before copying application source.

Why COPY order matters

If you COPY . . before installing dependencies, editing a single source file invalidates the cache and forces a full reinstall on every build. Splitting the copy keeps the expensive install step cached.

Multi-Stage Builds

Multi-stage builds use one stage to compile and a second, minimal stage to run. Build tools, compilers, and source never reach the final image, which stays small and has a smaller attack surface.

dockerfile
# syntax=docker/dockerfile:1

# ---- build stage ----
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/server

# ---- runtime stage ----
FROM gcr.io/distroless/static-debian12
COPY --from=build /bin/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]

The Docker CLI

These commands cover the everyday build, run, inspect, and debug loop.

bash
docker build -t myapp:1.0 .          # build and tag from ./Dockerfile
docker run -d --name web -p 8080:3000 myapp:1.0  # run detached, map port
docker ps                            # running containers
docker ps -a                         # include stopped ones
docker logs -f web                   # follow container logs
docker exec -it web sh               # open a shell inside a container
docker stop web && docker rm web     # stop then remove
docker images                        # list local images
docker rmi myapp:1.0                 # remove an image
docker system prune -af              # reclaim space (dangling + unused)

Volumes: Persisting Data

Use named volumes for data Docker manages (databases), and bind mounts to map a host directory into a container (local development).

bash
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:17

# bind mount host code for live reload during development
docker run -it -v "$(pwd)":/app -w /app node:22-alpine npm run dev

docker volume ls          # list volumes
docker volume inspect pgdata
docker volume rm pgdata   # delete (must not be in use)

Networks

Containers on the same user-defined bridge network can reach each other by container name via built-in DNS. This is how a web app finds its database without hardcoding IPs.

bash
docker network create appnet
docker run -d --name db --network appnet postgres:17
docker run -d --name api --network appnet -e DB_HOST=db myapi:1.0
# 'api' resolves 'db' by name over appnet's DNS
docker network ls
docker network inspect appnet

Docker Compose

Compose defines multi-container applications in a single YAML file. One command brings the whole stack up, with a network created automatically so services find each other by name.

yaml
# compose.yaml
services:
  api:
    build: .
    ports:
      - "8080:3000"
    environment:
      DB_HOST: db
      DB_PASSWORD: ${DB_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
volumes:
  pgdata:
bash
docker compose up -d          # build + start in background
docker compose ps             # status of services
docker compose logs -f api    # follow one service's logs
docker compose exec db psql -U postgres
docker compose down -v        # stop and remove, including volumes

.dockerignore

Like .gitignore, this file keeps unneeded files out of the build context. That speeds up builds and prevents secrets or bulky directories from leaking into images.

text
# .dockerignore
node_modules
.git
.env
*.log
dist
Dockerfile
docker-compose*.yml

Image Size Optimization

Smaller images pull faster, cost less to store, and expose fewer vulnerabilities. Key techniques:

TechniqueEffect
Slim/alpine or distroless baseStarts hundreds of MB smaller
Multi-stage buildsDrops compilers and source
Combine RUN steps, clean cachesFewer, leaner layers
.dockerignoreSmaller build context
dockerfile
# chain apt in one layer and clean up to avoid caching package lists
RUN apt-get update && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

docker history myapp:1.0     # see the size of each layer
docker scout cves myapp:1.0  # scan the image for known vulnerabilities

Registries

A registry stores and distributes images. Docker Hub is the default; teams also use GitHub Container Registry (GHCR), Amazon ECR, or a private registry. Tag with the full registry path, then push.

bash
docker login ghcr.io                       # authenticate
docker tag myapp:1.0 ghcr.io/acme/myapp:1.0
docker push ghcr.io/acme/myapp:1.0
docker pull ghcr.io/acme/myapp:1.0

# build for multiple architectures and push in one step
docker buildx build --platform linux/amd64,linux/arm64 \
  -t ghcr.io/acme/myapp:1.0 --push .

Practice Exercises

  1. Write a Dockerfile for a small Node or Python app that copies dependency manifests before source code, and explain which layer is reused when you change a source file.
  2. Convert a single-stage Go or Java build into a multi-stage build with a distroless runtime stage, then compare image sizes with docker images.
  3. Run a Postgres container backed by a named volume, insert some data, remove and recreate the container, and confirm the data survived.
  4. Create a user-defined network with an API and a database container, and verify the API can reach the database by container name.
  5. Write a compose.yaml with a web service and a database that uses depends_on with a healthcheck, then bring it up with a single command.
  6. Add a .dockerignore, rebuild, and use docker history and docker scout cves to measure the size and vulnerability improvements.

Section navigation