Docker & Containerization

Images, containers, volumes, networks and multi-stage builds — containerize a service the production-grade way.

BeginnerUpdated 2026-10-2851K views

What a container actually is

A container is a normal process on the host, isolated by kernel features: namespaces hide the view (own PID list, own network stack, own mount tree), cgroups limit the budget (CPU, memory, IO). There is no guest kernel — that is why containers start in milliseconds.

ConceptAnalogyTechnology
Image类(class)分层只读文件系统 + 元数据
Container实例(instance)运行中的进程 + 可写层
Volume外接硬盘宿主机目录/磁盘挂进容器
Network虚拟交换机bridge / overlay(vxlan) / host
RegistryMaven/npm 仓库Docker Hub / Harbor / 云厂商
docker run -d --name api \
  -p 8080:8080 \
  -v $(pwd)/logs:/app/logs \
  --memory=512m --cpus=1 \
  --restart=unless-stopped \
  myapp:1.2.0

docker logs -f --tail 200 api     # 跟日志
docker exec -it api sh            # 进容器排查
docker stats --no-stream          # 资源占用快照

Writing a production-grade Dockerfile

Most Dockerfile problems come from copying dev-machine habits: huge images, secrets baked into layers, and a rebuild that reinstalls every dependency when only one line of code changed.

# ---- 构建stage ----
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download           # ← 依赖层单独先装,利用缓存
COPY . .
RUN CGO_ENABLED=0 go build -ldflags='-s -w' -o /out/api ./cmd/api

# ---- 运行stage ----
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/api /api
EXPOSE 8080
USER nonroot:nonroot          # 别用 root 跑
ENTRYPOINT ["/api"]
  • Multi-stage build: compiler toolchain stays in the first stage, final image is ~20MB, not 900MB
  • Order matters: copy go.mod/go.sum and download before COPY . . so code changes don't bust the dependency cache
  • ENV creds in Dockerfile = leaked to anyone who can docker pull / docker history
  • dumb-init / tini as PID 1: the kernel's PID 1 has special signal semantics, plain app binaries may ignore SIGTERM
Layer caching works because each instruction is a cached layer keyed by its inputs. Putting COPY . . before RUN go mod download means every code change invalidates the download layer. This single ordering mistake turns 10s builds into 3min builds.

Data that must not die: volumes & binds

The container writable layer is ephemeral — kill the container and it is gone. Anything persistent (database data, uploads, logs you care about) must live in a volume or a bind mount.

# 命名 volume:docker 管理,跨容器共享、可做卷级备份
 docker volume create pgdata
 docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:16

# bind 挂载:直接把宿主机目录映射进去(开发常用,生产不推荐权限混乱)
docker run -v /opt/app/config.yaml:/app/config.yaml:ro myapp
# 备份命名 volume
docker run --rm -v pgdata:/d -v $(pwd):/b alpine tar czf /b/pgdata.tgz -C /d .
Bind-mounting a single file with SELinux/AppArmor enabled can give permission errors even with correct rwx bits: the container label cannot read the host file. Use :z / :Z flags or stick to named volumes.

Networking and docker compose

Containers on the same user-defined bridge network resolve each other by container name — this is how a compose stack of api + db + cache finds itself without hard-coded IPs.

# docker-compose.yml
services:
  api:
    build: .
    ports: ["8080:8080"]
    environment:
      DSN: postgres://app:secret@db:5432/app?sslmode=disable
    depends_on:
      db: {condition: service_healthy}
    restart: unless-stopped
  db:
    image: postgres:16
    volumes: ["pgdata:/var/lib/postgresql/data"]
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10
volumes: {pgdata: {}}
depends_on alone only orders container START; without the healthcheck the API boots before Postgres accepts connections and you get a crash-loop at every docker compose up.
docker network ls
docker network create appnet
docker compose up -d --build       # 改代码后重建+启动
docker compose logs -f api
docker compose down               # 停掉并删容器(volume 保留)

Friends & Partners