Docker & Containerization
Images, containers, volumes, networks and multi-stage builds — containerize a service the production-grade way.
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.
| Concept | Analogy | Technology |
|---|---|---|
| Image | 类(class) | 分层只读文件系统 + 元数据 |
| Container | 实例(instance) | 运行中的进程 + 可写层 |
| Volume | 外接硬盘 | 宿主机目录/磁盘挂进容器 |
| Network | 虚拟交换机 | bridge / overlay(vxlan) / host |
| Registry | Maven/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
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 .
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: {}}
docker network ls
docker network create appnet
docker compose up -d --build # 改代码后重建+启动
docker compose logs -f api
docker compose down # 停掉并删容器(volume 保留)容器到底是什么
容器就是宿主机上的普通进程,靠内核能力隔离:namespaces 隔离视图(独立 PID 列表、独立网络栈、独立挂载树),cgroups 限制配额(CPU、内存、IO)。没有客户机内核——这就是容器毫秒级启动的原因。
| 概念 | 类比 | 底层技术 |
|---|---|---|
| Image | 类(class) | 分层只读文件系统 + 元数据 |
| Container | 实例(instance) | 运行中的进程 + 可写层 |
| Volume | 外接硬盘 | 宿主机目录/磁盘挂进容器 |
| Network | 虚拟交换机 | bridge / overlay(vxlan) / host |
| Registry | Maven/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 # 资源占用快照
写一个生产级的 Dockerfile
大多数 Dockerfile 问题都来自把开发机的习惯照搬过来:镜像巨大、密钥被打进层里、改一行代码就全量重装依赖。
# ---- 构建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"]
- 多阶段构建:编译工具链留在第一阶段,最终镜像约 20MB 而不是 900MB
- 顺序很重要:先 COPY 依赖描述并下载,再 COPY 源码,改代码才不会让依赖缓存失效
- Dockerfile 里写密钥 = 谁能 pull/history 谁就能看到
- 用 dumb-init/tini 做 PID 1:内核对 PID 1 的信号处理特殊,普通应用二进制可能不响应 SIGTERM
不能丢的数据:volume 与挂载
容器可写层是一次性的——容器删除即消失。所有要持久化的东西(数据库数据、上传文件、在乎的日志)必须放进 volume 或 bind 挂载。
# 命名 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 .
网络与 docker compose
同一用户自定义 bridge 网络上的容器,可以用容器名互相解析——这就是 compose 编排 api + db + cache 时各服务不用写死 IP 的原理。
# 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: {}}
docker network ls
docker network create appnet
docker compose up -d --build # 改代码后重建+启动
docker compose logs -f api
docker compose down # 停掉并删容器(volume 保留)