Dockerfile 写作精讲 / 14 - 常见构建模式
14 - 常见构建模式:Builder、Rootless 与 Init 系统
14.1 Builder 模式
Builder 模式是最基础的多阶段构建模式——将构建环境与运行环境完全分离。
基本思路
Builder 阶段: 生产阶段:
┌────────────────────┐ ┌──────────────────┐
│ 完整的编译工具链 │ │ 精简的运行时 │
│ 源代码 │ ──▶ │ 编译后的产物 │
│ 依赖安装 │ │ 运行时依赖 │
│ 测试框架 │ │ │
└────────────────────┘ └──────────────────┘
通用 Builder 模式模板
# syntax=docker/dockerfile:1
# ===== Builder =====
FROM <构建镜像> AS builder
WORKDIR /src
# 1. 安装依赖(利用缓存)
COPY <依赖清单> .
RUN <安装依赖命令>
# 2. 复制源码
COPY . .
# 3. 构建
RUN <构建命令>
# ===== Production =====
FROM <运行时镜像>
WORKDIR /app
# 4. 仅复制产物
COPY --from=builder /src/<产物路径> .
# 5. 安全配置
USER <非root用户>
EXPOSE <端口>
HEALTHCHECK ...
CMD ...
单元测试阶段
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 测试阶段
FROM builder AS tester
RUN --mount=type=cache,target=/root/.cache/go-build \
go test ./... -v -race -coverprofile=coverage.out
# 生产阶段
FROM builder AS compiler
RUN CGO_ENABLED=0 go build -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=compiler /server /server
ENTRYPOINT ["/server"]
# 仅运行测试
docker build --target tester -t myapp:test .
# 仅构建生产镜像(跳过测试)
docker build --target production -t myapp:prod .
14.2 Rootless 模式
Rootless 容器指的是容器内的进程以非 root 用户身份运行,同时 Docker 引擎本身也可以以非 root 用户运行。
容器内 Rootless
FROM python:3.12-slim
# 创建非 root 用户
RUN groupadd -g 1001 appuser && \
useradd -u 1001 -g appuser --no-create-home --shell /bin/bash appuser
# 设置工作目录权限
WORKDIR /app
COPY --chown=appuser:appuser . .
# 安装依赖(以 root,然后切回非 root)
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
CMD ["python", "app.py"]
Distroless 的 nonroot 标签
# 使用预置的 nonroot 用户(UID 65532)
FROM gcr.io/distroless/static-debian12:nonroot
COPY server /server
# 不需要 USER 指令,nonroot 标签已设置
ENTRYPOINT ["/server"]
Docker 引擎 Rootless 模式
# 安装 rootless Docker
dockerd-rootless-setuptool.sh install
# 验证
docker context use rootless
docker info | grep "Security Options"
# 输出: rootless
# rootless 模式的限制:
# - 不能绑定 < 1024 端口(除非配置 sysctl)
# - 不支持某些存储驱动
# - 不支持 --net=host 的完全功能
Rootless 限制与解决
| 限制 | 解决方案 |
|---|---|
| 无法绑定 < 1024 端口 | 使用端口映射或 sysctl net.ipv4.ip_unprivileged_port_start=0 |
| 存储权限问题 | 使用 user namespace 或配置 subuid/subgid |
| 网络性能 | 使用 slirp4netns 或 pasta 驱动 |
| cgroup 限制 | 使用 cgroup v2 + systemd |
14.3 Init 系统模式
为什么需要 Init 系统
没有 Init 系统:
PID 1 ──▶ app(无法回收僵尸进程,信号传播可能有问题)
有 Init 系统:
PID 1 ──▶ tini/dumb-init
├── PID 2 ──▶ app(接收正确转发的信号)
└── 回收僵尸进程
使用 tini
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends tini && \
rm -rf /var/lib/apt/lists/*
COPY app /app
ENTRYPOINT ["tini", "--"]
CMD ["/app"]
使用 dumb-init
FROM alpine:3.19
RUN apk add --no-cache dumb-init
COPY app /app
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/app"]
tini vs dumb-init vs s6
| 工具 | 体积 | 功能 | 适用场景 |
|---|---|---|---|
| tini | ~25KB | 基础 init + 僵尸回收 | 单进程容器 |
| dumb-init | ~300KB | 信号转发 + 僵尸回收 | 单进程容器 |
| s6-overlay | ~2MB | 完整进程管理 | 多进程容器 |
| supervisord | ~5MB | Python 进程管理 | 多进程容器(Python 生态) |
Docker 内置 –init
# 使用 Docker 内置的 --init 标志(自动注入 tini)
docker run --init myapp
# 等价于
docker run --entrypoint tini myapp -- <original-cmd>
14.4 Sidecar 模式
健康检查 Sidecar
# 健康检查脚本
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
#!/bin/bash
# healthcheck.sh
set -e
# 检查应用进程是否在运行
if ! pgrep -x "myapp" > /dev/null; then
echo "Application process not found"
exit 1
fi
# 检查端口是否可访问
if ! nc -z localhost 8080; then
echo "Port 8080 not accessible"
exit 1
fi
# 检查 HTTP 健康端点
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "Health endpoint returned $HTTP_CODE"
exit 1
fi
echo "Health check passed"
exit 0
14.5 配置注入模式
环境变量 + 默认值
FROM node:20-alpine
WORKDIR /app
COPY . .
# 提供合理的默认值
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
ENV DB_HOST=localhost
ENV DB_PORT=5432
CMD ["node", "server.js"]
配置文件模板
FROM nginx:alpine
COPY nginx.conf.template /etc/nginx/templates/
COPY default.conf.template /etc/nginx/templates/
# Nginx 官方镜像支持环境变量模板替换
# 使用 envsubst 自动替换 ${VAR} 占位符
ENV BACKEND_HOST=backend
ENV BACKEND_PORT=8080
EXPOSE 80
# default.conf.template
upstream backend {
server ${BACKEND_HOST}:${BACKEND_PORT};
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
entrypoint 脚本注入配置
FROM python:3.12-slim
WORKDIR /app
COPY . .
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "app.py"]
#!/bin/bash
set -e
# 根据环境变量生成配置文件
cat > /app/config.yaml <<EOF
database:
host: ${DB_HOST:-localhost}
port: ${DB_PORT:-5432}
name: ${DB_NAME:-myapp}
user: ${DB_USER:-postgres}
logging:
level: ${LOG_LEVEL:-info}
EOF
# 如果需要初始化数据库
if [ "${INIT_DB}" = "true" ]; then
python manage.py init-db
fi
# 执行主命令
exec "$@"
14.6 模块化 Dockerfile
使用 BuildKit 导入外部 Dockerfile 片段
# syntax=docker/dockerfile:1
# 导入外部 Dockerfile 片段
# FROM scratch
# COPY --from=dep-creator /deps /deps
# 或使用 ARG 动态选择基础镜像
ARG BASE_IMAGE=python:3.12-slim
FROM ${BASE_IMAGE}
Docker Compose 中的模块化
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
args:
- APP_VERSION=${APP_VERSION:-dev}
app-debug:
build:
context: .
dockerfile: Dockerfile
target: debug
14.7 模式选型指南
| 模式 | 适用场景 | 复杂度 | 镜像体积 |
|---|---|---|---|
| 单阶段 | 简单脚本/原型 | 低 | 大 |
| Builder 模式 | 编译型语言 | 中 | 小 |
| Rootless 模式 | 安全要求高 | 低 | 无额外开销 |
| Init 系统 | 多进程/僵尸进程 | 低 | 极小 |
| 配置注入 | 多环境部署 | 中 | 无额外开销 |
14.8 扩展阅读
上一章:13 - 语言最佳实践 下一章:15 - 镜像瘦身 — 层合并、UPX 压缩与符号剥离。