第 15 章 · Docker 多阶段构建
本章目标:编写 api-go-demo 多阶段 Dockerfile;非 root 运行;配置 HEALTHCHECK;编写 docker-compose 编排 MySQL + Redis + API(+ Worker 选修);.dockerignore 与十二要素配置;对照 fastapi-web ch19 与 paas 部署;为 ch16 毕业交付做准备。
学时建议:4~5 小时(含 2 小时跟练)
前置:完成 gin-web ch14;本机 Docker Desktop。
15.1 场景说明:单二进制容器化
Go 编译为静态二进制,runtime 镜像可极小:
源码 ──multi-stage build──► registry.example.com/api-go-demo:1.0.0 ──► compose / K8s
| 对比 | Python svc-demo | Go api-go-demo |
|---|---|---|
| 运行时 | venv + 解释器 | 单二进制 |
| 典型镜像 | 200MB+ | < 30MB(distroless) |
| 启动 | 秒级 | 毫秒级 |
| 用户 | app nonroot | nonroot |
api-go-demo/
├── Dockerfile
├── Dockerfile.worker # 选修
├── docker-compose.yml
├── docker-compose.prod.yml # 选修
└── .dockerignore
虚构 registry.example.com、api.example.com;严禁真实密钥进镜像或 Git。
15.2 多阶段 Dockerfile(完整)
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /src
RUN apk add --no-cache git ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=${VERSION}" \
-o /out/server ./cmd/server
# 选修:同一阶段编译 worker
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/worker ./cmd/worker
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
WORKDIR /app
COPY --from=builder /out/server /app/server
COPY --from=builder /src/configs /app/configs
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
ENV TZ=UTC
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/app/server", "-health-check"]
# 若 distroless 无 wget,改用 binary 内置 -health-check 或 k8s probe 代替
# 见 15.6 备选 HEALTHCHECK
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]
| 阶段 | 作用 |
|---|---|
| builder | toolchain、编译、go mod cache |
| runtime | 仅二进制 + CA + 配置 |
CGO_ENABLED=0:纯静态链接,distroless 无 glibc 依赖。
15.3 内置 health-check 模式(distroless 友好)
distroless 无 shell/curl,建议在 cmd/server 支持:
// cmd/server/main.go
func main() {
if len(os.Args) > 1 && os.Args[1] == "-health-check" {
resp, err := http.Get("http://127.0.0.1:8080/health")
if err != nil || resp.StatusCode != 200 {
os.Exit(1)
}
os.Exit(0)
}
// 正常启动...
}
或 不在 Dockerfile HEALTHCHECK,仅依赖 compose/K8s probe(见 15.5)。
15.4 .dockerignore
.git
.gitignore
.env
.env.*
!.env.example
bin/
logs/
tmp/
**/*_test.go
tests/
coverage.out
*.md
!README.md
docker-compose*.yml
Dockerfile*
.vscode
.idea
docs/swagger.json.bak
减小 context,避免 .env 进镜像。
15.5 docker-compose.yml(开发全栈)
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: api_demo
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppass"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
APP_ENV: prod
APP_PORT: "8080"
DATABASE_URL: "root:pass@tcp(mysql:3306)/api_demo?charset=utf8mb4&parseTime=True&loc=Local"
REDIS_ADDR: "redis:6379"
JWT_SECRET: "docker-dev-secret-change-in-prod-min-32-chars"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "/app/server", "-health-check"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
worker:
build:
context: .
dockerfile: Dockerfile
entrypoint: ["/app/worker"]
environment:
REDIS_ADDR: "redis:6379"
depends_on:
- redis
- api
volumes:
mysql_data:
docker compose up --build -d
docker compose ps
curl -sf http://127.0.0.1:8080/health
curl -sf http://127.0.0.1:8080/api/v1/products/go-handbook