强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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~5MBPython 进程管理多进程容器(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 压缩与符号剥离。