Dockerfile 写作精讲 / 18 - 生产最佳实践
18 - 生产最佳实践:CI/CD 集成、安全基线与维护策略
18.1 生产环境 Dockerfile 清单
在将 Dockerfile 投入生产之前,确保满足以下所有检查项:
基础要求
| 检查项 | 标准 | 验证方式 |
|---|
| 固定基础镜像版本 | 使用 digest 或具体版本标签 | FROM python:3.12.2-slim-bookworm |
| 非 root 用户 | 使用 USER 指令 | USER appuser |
| 健康检查 | HEALTHCHECK 指令 | HEALTHCHECK CMD ... |
| 多阶段构建 | 构建与运行分离 | 使用 AS builder |
| 无秘密泄露 | 使用 BuildKit secrets | --mount=type=secret |
| .dockerignore | 排除不必要文件 | 检查 .dockerignore 文件 |
| 体积合理 | 在预期范围内 | docker image inspect |
| 无高危漏洞 | 扫描通过 | trivy image |
Dockerfile 模板(生产级)
# syntax=docker/dockerfile:1
# ===========================================================
# 生产级 Dockerfile 模板
# ===========================================================
# ----- 全局构建参数 -----
ARG BASE_IMAGE=python
ARG BASE_TAG=3.12.2-slim-bookworm
# ===========================================================
# 阶段一:依赖安装
# ===========================================================
FROM ${BASE_IMAGE}:${BASE_TAG} AS deps
# 系统依赖(合并安装 + 清理)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Python 依赖
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# ===========================================================
# 阶段二:测试(可选,CI 中使用)
# ===========================================================
FROM deps AS tester
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
RUN pytest tests/ -v --tb=short
# ===========================================================
# 阶段三:生产镜像
# ===========================================================
FROM deps AS production
# 安全:创建非 root 用户
RUN groupadd -g 1001 appgroup && \
useradd -u 1001 -g appgroup --no-create-home --shell /bin/bash appuser
WORKDIR /app
# 复制应用代码
COPY --chown=appuser:appuser . .
# 元数据
LABEL maintainer="devops@example.com" \
version="${APP_VERSION}" \
description="Production image"
# 切换用户
USER appuser
# 端口声明
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 启动命令(使用 exec 形式确保 PID 1 正确)
CMD ["gunicorn", "app:app", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120", \
"--access-logfile", "-"]
18.2 CI/CD 集成策略
完整的 CI/CD 流水线
代码提交
│
▼
┌─────────────────┐
│ 代码检查 │ Hadolint, ESLint, Black
├─────────────────┤
│ 单元测试 │ pytest, jest, go test
├─────────────────┤
│ 镜像构建 │ docker buildx build
├─────────────────┤
│ 安全扫描 │ Trivy, Grype
├─────────────────┤
│ 镜像签名 │ Cosign
├─────────────────┤
│ 推送 Registry │ docker push
├─────────────────┤
│ 部署 Staging │ kubectl apply / docker compose
├─────────────────┤
│ 集成测试 │ API 测试, E2E 测试
├─────────────────┤
│ 部署 Production │ 渐进式发布
└─────────────────┘
GitHub Actions 完整流水线
name: Build, Test & Deploy
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ===== 代码检查 =====
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Hadolint
uses: hadolint/hadolint-action@v3.1.0
- name: Python lint
run: |
pip install ruff
ruff check .
# ===== 构建与测试 =====
build:
runs-on: ubuntu-latest
needs: lint
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 构建测试镜像
- name: Build test image
uses: docker/build-push-action@v5
with:
context: .
target: tester
load: true
tags: ${{ env.IMAGE_NAME }}:test
# 运行测试
- name: Run tests
run: docker run --rm ${{ env.IMAGE_NAME }}:test
# 构建生产镜像
- name: Build production image
uses: docker/build-push-action@v5
with:
context: .
target: production
load: true
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
APP_VERSION=${{ github.sha }}
# 镜像体积检查
- name: Check image size
run: |
SIZE=$(docker image inspect ${{ env.IMAGE_NAME }}:${{ github.sha }} --format='{{.Size}}')
SIZE_MB=$((SIZE / 1024 / 1024))
echo "Image size: ${SIZE_MB}MB"
if [ "$SIZE_MB" -gt 200 ]; then
echo "::error::Image size exceeds 200MB limit"
exit 1
fi
# 安全扫描
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
# 推送镜像
- name: Push image
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
run: |
docker tag ${{ env.IMAGE_NAME }}:${{ github.sha }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker tag ${{ env.IMAGE_NAME }}:${{ github.sha }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# 签名镜像
- name: Install Cosign
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
uses: sigstore/cosign-installer@v3
- name: Sign image
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
18.3 安全基线
CIS Docker Benchmark 关键规则
| 规则 | 描述 | Dockerfile 实现 |
|---|
| 4.1 | 以非 root 用户运行 | USER appuser |
| 4.2 | 使用可信基础镜像 | 使用官方镜像 + digest |
| 4.6 | 添加 HEALTHCHECK | HEALTHCHECK CMD ... |
| 4.7 | 不安装不必要的软件 | --no-install-recommends |
| 4.9 | 使用 COPY 而非 ADD | 仅在需要解压时使用 ADD |
| 4.10 | 不在镜像中存储秘密 | BuildKit secrets |
| 4.11 | 安装安全更新 | 定期更新基础镜像 |
Docker Daemon 安全配置
// /etc/docker/daemon.json
{
"features": {
"buildkit": true
},
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"no-new-privileges": true,
"userns-remap": "default",
"live-restore": true,
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 65536
}
}
}
容器运行时安全
# 以只读文件系统运行
docker run --read-only --tmpfs /tmp myapp
# 限制资源
docker run --memory=512m --cpus=1.0 myapp
# 禁用特权提升
docker run --security-opt=no-new-privileges myapp
# 只读根文件系统 + 临时写入目录
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid \
--tmpfs /var/run:rw,noexec,nosuid \
myapp
# 使用 seccomp 限制系统调用
docker run --security-opt seccomp=custom-seccomp.json myapp
# AppArmor
docker run --security-opt apparmor=docker-default myapp
18.4 镜像版本管理
标签策略
# 语义化版本
docker tag myapp:latest myapp:1.2.3
docker tag myapp:latest myapp:1.2
docker tag myapp:latest myapp:1
# Git commit SHA
docker tag myapp:latest myapp:sha-abc1234
# Git tag
docker tag myapp:latest myapp:v1.2.3
# 日期标签
docker tag myapp:latest myapp:20240115
# 环境标签
docker tag myapp:latest myapp:staging
docker tag myapp:latest myapp:production
版本矩阵
| 标签 | 更新频率 | 可重现性 | 用途 |
|---|
latest | 每次构建 | ❌ | 仅限本地开发 |
1.2.3 | 发布时 | ✅ | 生产部署 |
sha-abc1234 | 每次提交 | ✅ | CI/CD 流水线 |
digest | 自动 | ✅✅ | 安全要求极高 |
Dependabot / Renovate 自动更新
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "devops-team"
labels:
- "dependencies"
- "docker"
18.5 镜像维护策略
定期重建
# 定期重建(获取安全补丁)
# .github/workflows/rebuild.yml
name: Weekly Rebuild
on:
schedule:
- cron: '0 2 * * 1' # 每周一凌晨 2 点
workflow_dispatch:
jobs:
rebuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
no-cache: true # 强制重建,获取最新补丁
cache-from: type=gha
cache-to: type=gha,mode=max
漏洞监控
# GitHub Security Alerts
# .github/workflows/vuln-scan.yml
name: Vulnerability Scan
on:
schedule:
- cron: '0 6 * * *' # 每天凌晨 6 点
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:latest
format: 'sarif'
output: 'trivy-results.sarif'
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
18.6 监控与可观测性
容器日志
# 将日志输出到 stdout/stderr(容器最佳实践)
# 不要将日志写入文件
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--access-logfile", "-"]
Prometheus 指标暴露
EXPOSE 8000 # 应用端口
EXPOSE 9090 # 指标端口(可选,分离端口更安全)
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1
18.7 灾难恢复
镜像备份
# 导出镜像为 tar
docker save myapp:latest | gzip > myapp-latest.tar.gz
# 导入镜像
docker load < myapp-latest.tar.gz
# 从 Registry 重新拉取
docker pull ghcr.io/myorg/myapp:sha-abc1234
回滚策略
# 快速回滚到上一个版本
docker pull ghcr.io/myorg/myapp:sha-old123
docker compose up -d
# Kubernetes 回滚
kubectl rollout undo deployment/myapp
18.8 附录:Dockerfile 编写速查表
| 指令 | 最佳实践 |
|---|
FROM | 使用具体版本标签或 digest,优先选择 Alpine/Distroless |
RUN | 合并相关操作,同一层清理缓存 |
COPY | 优先复制依赖清单,后复制源码 |
ADD | 仅在需要自动解压时使用 |
ENV | 用于运行时配置,不存储秘密 |
ARG | 用于构建参数,配合 ENV 持久化 |
CMD | 使用 exec 形式 |
ENTRYPOINT | 使用 exec 形式,配合 tini |
EXPOSE | 声明文档,不发布端口 |
USER | 始终切换到非 root 用户 |
WORKDIR | 使用绝对路径 |
HEALTHCHECK | 为所有服务添加健康检查 |
LABEL | 添加维护者、版本等元数据 |
VOLUME | 声明数据目录 |
SHELL | 仅在需要时修改默认 shell |
18.9 扩展阅读
上一章:17 - Docker Compose 集成
完成全部 18 章学习后,你已经掌握了从基础指令到生产级部署的完整 Dockerfile 知识体系。建议收藏本教程作为日常工作参考。