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

Dockerfile 写作精讲 / 16 - 测试与验证

16 - 测试与验证:Hadolint、容器内测试与 CI 集成

16.1 为什么需要测试 Dockerfile

Dockerfile 和镜像与代码一样需要测试:

测试维度目标工具
语法规范遵循最佳实践Hadolint, dockle
安全合规无已知漏洞和配置问题Trivy, Grype
功能验证应用在容器中正常运行容器内测试
构建验证Dockerfile 能成功构建CI 构建测试
体积检查镜像体积在预期范围内自定义脚本

16.2 Hadolint:Dockerfile 静态分析

安装与使用

# Docker 方式运行
docker run --rm -i hadolint/hadolint < Dockerfile

# 安装二进制
# macOS
brew install hadolint

# Linux
wget -O /usr/local/bin/hadolint \
    https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint

# 基本使用
hadolint Dockerfile

# 输出 JSON
hadolint --format json Dockerfile

规则级别

级别说明示例
error必须修复DL3009: 删除 apt lists
warning建议修复DL3018: 固定包版本
info信息提示DL3006: 固定基础镜像版本
style代码风格DL3003: 使用 WORKDIR 而非 cd
ignore忽略自定义忽略

常见规则

# DL3008: 固定 apt 包版本(warning)
# ❌
RUN apt-get install -y curl
# ✅
RUN apt-get install -y curl=7.88.1-10+deb12u5

# DL3009: 清理 apt lists(error)
# ❌
RUN apt-get update && apt-get install -y curl
# ✅
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# DL3025: CMD 使用 JSON 形式(warning)
# ❌
CMD python app.py
# ✅
CMD ["python", "app.py"]

# DL3003: 使用 WORKDIR 而非 cd(style)
# ❌
RUN cd /app && make
# ✅
WORKDIR /app
RUN make

配置文件

# .hadolint.yaml
ignoredRules:
  - DL3008  # 忽略"固定包版本"规则
  - DL3013  # 忽略"固定 pip 包版本"

trustedRegistries:
  - docker.io
  - ghcr.io

override:
  warning:
    - DL3006  # 将"固定基础镜像版本"从 info 提升到 warning

忽略特定行

# 忽略单条规则
RUN apt-get update && apt-get install -y curl # hadolint ignore=DL3008

# 忽略多条规则
RUN apt-get update && apt-get install -y curl # hadolint ignore=DL3008 DL3009

# 忽略文件中所有规则(不推荐)
# hadolint global ignore=DL3008

16.3 Dockle:容器镜像安全检查

# 安装
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
    goodwithtech/dockle myapp:latest

# 输出 JSON
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
    goodwithtech/dockle --format json myapp:latest

# 退出码(CI 集成)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
    goodwithtech/dockle --exit-code 1 --exit-level warn myapp:latest

Dockle 检查项

检查项说明
CIS-DI-0001创建非 root 用户
CIS-DI-0002使用可信基础镜像
CIS-DI-0005启用 Content Trust
CIS-DI-0006添加 HEALTHCHECK
CIS-DI-0008移除 setuid/setgid 位
CIS-DI-0009使用 COPY 而非 ADD

16.4 容器内测试

在容器中运行测试套件

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt requirements-dev.txt ./
RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
COPY . .

# 测试阶段
FROM builder AS tester
# 在容器内运行测试
RUN pytest tests/ -v --junitxml=/test-results/results.xml \
    --cov=app --cov-report=html:/test-results/coverage

# 生产阶段
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /app/app ./app
CMD ["gunicorn", "app:app"]
# 构建并运行测试
docker build --target tester -t myapp:test .

# 提取测试结果
docker create --name test-results myapp:test
docker cp test-results:/test-results ./test-results
docker rm test-results

Node.js 集成测试

FROM node:20-alpine AS tester
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# 运行 lint
RUN npm run lint

# 运行单元测试
RUN npm test

# 运行构建(验证编译通过)
RUN npm run build

Go 测试 + 覆盖率

FROM golang:1.22-alpine AS tester
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# 运行测试并生成覆盖率报告
RUN go test ./... -v -race -coverprofile=coverage.out -covermode=atomic

# 输出覆盖率摘要
RUN go tool cover -func=coverage.out

# 生产构建
FROM tester AS builder
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
ENTRYPOINT ["/server"]

16.5 构建验证测试

镜像体积检查

#!/bin/bash
# check-image-size.sh
MAX_SIZE_MB=100
IMAGE_NAME="myapp:latest"

# 构建镜像
docker build -t "$IMAGE_NAME" .

# 获取镜像大小(字节)
SIZE_BYTES=$(docker image inspect "$IMAGE_NAME" --format='{{.Size}}')
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))

echo "Image size: ${SIZE_MB}MB (limit: ${MAX_SIZE_MB}MB)"

if [ "$SIZE_MB" -gt "$MAX_SIZE_MB" ]; then
    echo "FAIL: Image exceeds size limit!"
    exit 1
fi

echo "PASS: Image size is within limit."

容器启动健康检查

#!/bin/bash
# smoke-test.sh
IMAGE_NAME="myapp:latest"
CONTAINER_NAME="smoke-test-$$"

# 启动容器
docker run -d --name "$CONTAINER_NAME" -p 0:8080 "$IMAGE_NAME"

# 等待启动
sleep 5

# 检查容器是否在运行
if ! docker ps | grep -q "$CONTAINER_NAME"; then
    echo "FAIL: Container exited unexpectedly"
    docker logs "$CONTAINER_NAME"
    exit 1
fi

# 获取随机映射的端口
PORT=$(docker port "$CONTAINER_NAME" 8080 | cut -d: -f2)

# 检查健康端点
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/health")
if [ "$HTTP_CODE" != "200" ]; then
    echo "FAIL: Health check returned $HTTP_CODE"
    docker logs "$CONTAINER_NAME"
    exit 1
fi

echo "PASS: Container is healthy"

# 清理
docker rm -f "$CONTAINER_NAME"

安全检查脚本

#!/bin/bash
# security-check.sh
IMAGE_NAME="myapp:latest"

echo "=== Dockerfile Lint (Hadolint) ==="
hadolint Dockerfile || exit 1

echo "=== Vulnerability Scan (Trivy) ==="
trivy image --exit-code 1 --severity HIGH,CRITICAL "$IMAGE_NAME" || exit 1

echo "=== Security Check (Dockle) ==="
goodwithtech/dockle --exit-code 1 --exit-level warn "$IMAGE_NAME" || exit 1

echo "=== All security checks passed ==="

16.6 CI/CD 集成

GitHub Actions 完整示例

# .github/workflows/docker-ci.yml
name: Docker CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Hadolint
        uses: hadolint/[email protected]
        with:
          dockerfile: Dockerfile

  build-and-test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Build test image
        uses: docker/build-push-action@v5
        with:
          context: .
          target: tester
          load: true
          tags: myapp:test
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Run tests
        run: docker run --rm myapp:test

      - name: Build production image
        uses: docker/build-push-action@v5
        with:
          context: .
          target: production
          load: true
          tags: myapp:latest

      - name: Check image size
        run: |
          SIZE=$(docker image inspect myapp:latest --format='{{.Size}}')
          SIZE_MB=$((SIZE / 1024 / 1024))
          echo "Image size: ${SIZE_MB}MB"
          [ "$SIZE_MB" -lt 100 ] || { echo "Image too large!"; exit 1; }

      - name: Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:latest
          severity: HIGH,CRITICAL
          exit-code: '1'

      - name: Smoke test
        run: |
          docker run -d --name smoke -p 0:8080 myapp:latest
          sleep 5
          PORT=$(docker port smoke 8080 | cut -d: -f2)
          curl -f "http://localhost:${PORT}/health"
          docker rm -f smoke

  push:
    if: github.ref == 'refs/heads/main'
    needs: build-and-test
    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
          cache-from: type=gha
          cache-to: type=gha,mode=max

GitLab CI 示例

# .gitlab-ci.yml
stages:
  - lint
  - build
  - test
  - scan
  - push

hadolint:
  stage: lint
  image: hadolint/hadolint:latest
  script:
    - hadolint Dockerfile

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .

test:
  stage: test
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build --target tester -t myapp:test .
    - docker run --rm myapp:test

trivy-scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:$CI_COMMIT_SHA

push:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  only:
    - main
  script:
    - docker tag myapp:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest

16.7 测试策略矩阵

测试类型执行阶段工具失败策略
Dockerfile 代码检查PR 提交Hadolint阻断合并
构建测试PR 提交docker build阻断合并
单元测试PR 提交pytest/jest/go test阻断合并
镜像漏洞扫描PR + 主分支Trivy高危阻断
镜像体积检查PR + 主分支自定义脚本超限阻断
冒烟测试主分支自定义脚本阻断部署
安全合规检查主分支Dockle高危阻断
签名发布Cosign阻断发布

16.8 扩展阅读


上一章15 - 镜像瘦身 下一章17 - Docker Compose 集成 — Compose Build、多服务构建与环境变量。