Docker 容器化部署:一次构建,到处运行

Docker 容器化是现代应用部署的事实标准。对于 Go 应用而言,容器化的优势尤为明显:静态编译的二进制文件可以运行在极小的基础镜像中,最终镜像体积可以控制在 20MB 以内。但容器化不只是 docker builddocker run,生产级容器部署需要考虑镜像安全、多阶段构建优化、健康检查、资源限制、编排配置等方方面面。

本文将系统讲解从 Dockerfile 编写到生产编排的完整容器化实践。


Dockerfile 编写:从能用到极致

多阶段构建(标准做法)

多阶段构建是 Docker 18.06+ 引入的关键特性,它允许在一个 Dockerfile 中使用多个 FROM 指令,每个阶段可以基于不同的镜像,最终只复制需要的产物到最终镜像。

# ============================================
# 第一阶段:构建环境
# ============================================
FROM golang:1.24-alpine AS builder

# 安装构建依赖
RUN apk add --no-cache git ca-certificates tzdata

# 设置工作目录
WORKDIR /build

# 先复制 go.mod 和 go.sum,利用 Docker 缓存层
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# 复制源码
COPY . .

# 编译参数优化
ARG VERSION=dev
ARG BUILD_TIME
ARG GIT_COMMIT

# 生产级编译
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-s -w \
        -X main.version=${VERSION} \
        -X main.buildTime=${BUILD_TIME} \
        -X main.gitCommit=${GIT_COMMIT} \
        -extldflags '-static'" \
    -trimpath \
    -o agent \
    .

# 验证二进制文件
RUN chmod +x agent && \
    ls -lh agent && \
    file agent

# ============================================
# 第二阶段:运行环境(Distroless)
# ============================================
FROM gcr.io/distroless/static-debian12:nonroot

# 从 builder 阶段复制时区数据(Agent 可能需要处理时间相关逻辑)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# 复制 CA 证书(HTTPS 调用必需)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 复制编译好的二进制
COPY --from=builder /build/agent /agent

# 使用非 root 用户运行(安全加固)
USER nonroot:nonroot

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD ["/agent", "--health-check"] || exit 1

# 启动命令
ENTRYPOINT ["/agent"]
CMD ["--port", "8080"]

镜像体积对比

基础镜像最终体积安全性适用场景
golang:1.24~1GB仅开发
alpine:3.19~20MB通用生产
distroless/static~15MB推荐生产
scratch~12MB最高极简场景

使用 Alpine 的替代方案

如果需要 shell 调试(distroless 没有 shell),可以使用 Alpine:

# ============================================
# 第二阶段:Alpine 运行环境
# ============================================
FROM alpine:3.19

# 安装运行时依赖
RUN apk add --no-cache ca-certificates tzdata curl

# 创建非 root 用户
RUN addgroup -g 1000 agent && \
    adduser -u 1000 -G agent -s /bin/sh -D agent

# 复制二进制
COPY --from=builder /build/agent /usr/local/bin/agent

# 创建数据目录并设置权限
RUN mkdir -p /data && chown -R agent:agent /data

USER agent

WORKDIR /data

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

ENTRYPOINT ["agent"]
CMD ["--port", "8080"]

构建脚本

#!/bin/bash
# build-docker.sh

set -e

VERSION=${VERSION:-$(git describe --tags --always)}
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
GIT_COMMIT=$(git rev-parse --short HEAD)
IMAGE_NAME="my-adk-agent"
REGISTRY="gcr.io/my-project"

echo "Building ${IMAGE_NAME}:${VERSION}..."

# 构建镜像
docker build \
    --build-arg VERSION="${VERSION}" \
    --build-arg BUILD_TIME="${BUILD_TIME}" \
    --build-arg GIT_COMMIT="${GIT_COMMIT}" \
    -t "${IMAGE_NAME}:${VERSION}" \
    -t "${IMAGE_NAME}:latest" \
    .

# 安全扫描(使用 Trivy)
if command -v trivy &> /dev/null; then
    echo "Scanning image for vulnerabilities..."
    trivy image --severity HIGH,CRITICAL "${IMAGE_NAME}:${VERSION}"
fi

# 推送镜像(可选)
if [ "${PUSH:-false}" = "true" ]; then
    docker tag "${IMAGE_NAME}:${VERSION}" "${REGISTRY}/${IMAGE_NAME}:${VERSION}"
    docker tag "${IMAGE_NAME}:latest" "${REGISTRY}/${IMAGE_NAME}:latest"
    docker push "${REGISTRY}/${IMAGE_NAME}:${VERSION}"
    docker push "${REGISTRY}/${IMAGE_NAME}:latest"
fi

echo "Build complete: ${IMAGE_NAME}:${VERSION}"

构建和运行:从本地到生产

本地运行

# 构建
docker build -t my-adk-agent:latest .

# 运行(开发模式)
docker run -d \
    --name my-agent \
    -p 8080:8080 \
    -e GOOGLE_API_KEY="${GOOGLE_API_KEY}" \
    -e LOG_LEVEL=debug \
    -v $(pwd)/config.yaml:/data/config.yaml:ro \
    my-adk-agent:latest \
    --config /data/config.yaml

# 查看日志
docker logs -f my-agent

# 进入容器调试(仅 Alpine 镜像)
docker exec -it my-agent /bin/sh

# 停止并删除
docker stop my-agent && docker rm my-agent

生产运行

# 生产环境运行(更多限制)
docker run -d \
    --name my-agent \
    --restart unless-stopped \
    --read-only \
    --tmpfs /tmp:noexec,nosuid,size=100m \
    -p 8080:8080 \
    -e GOOGLE_API_KEY="${GOOGLE_API_KEY}" \
    -e LOG_LEVEL=info \
    -e LOG_FORMAT=json \
    -e MAX_CONCURRENT=200 \
    -e MAX_SESSIONS=50000 \
    --memory=2g \
    --memory-swap=2g \
    --cpus=2.0 \
    --pids-limit=10000 \
    --security-opt=no-new-privileges:true \
    --cap-drop=ALL \
    --cap-add=NET_BIND_SERVICE \
    my-adk-agent:latest

安全参数说明

参数作用安全意义
--read-only根文件系统只读防止运行时修改可执行文件
--tmpfs /tmp内存临时文件系统限制临时文件大小,noexec 防止执行
--memory内存限制防止 OOM 影响宿主机
--cpusCPU 限制防止 CPU 耗尽
--pids-limit进程数限制防止 fork 炸弹
--security-opt禁止提权防止容器逃逸
--cap-drop=ALL丢弃所有能力最小权限原则

Docker Compose:开发到生产的桥梁

基础配置

# docker-compose.yml
version: '3.8'

services:
  agent:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        VERSION: ${VERSION:-latest}
    image: my-adk-agent:${VERSION:-latest}
    container_name: my-agent
    restart: unless-stopped
    
    ports:
      - "8080:8080"
    
    environment:
      - PORT=8080
      - LOG_LEVEL=info
      - LOG_FORMAT=json
      - MAX_CONCURRENT=200
      - MAX_SESSIONS=50000
      - SESSION_TTL=24h
      - REDIS_URL=redis://redis:6379/0
      - GOOGLE_API_KEY=${GOOGLE_API_KEY}
    
    volumes:
      - ./config.yaml:/data/config.yaml:ro
      - agent-data:/data/sessions
    
    networks:
      - agent-network
    
    depends_on:
      redis:
        condition: service_healthy
    
    healthcheck:
      test: ["CMD", "/agent", "--health-check"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    
    # 资源限制
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    
    # 安全选项
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

  redis:
    image: redis:7-alpine
    container_name: agent-redis
    restart: unless-stopped
    
    volumes:
      - redis-data:/data
    
    networks:
      - agent-network
    
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  nginx:
    image: nginx:alpine
    container_name: agent-nginx
    restart: unless-stopped
    
    ports:
      - "80:80"
      - "443:443"
    
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
      - ./webui/dist:/usr/share/nginx/html:ro
    
    networks:
      - agent-network
    
    depends_on:
      - agent

volumes:
  agent-data:
  redis-data:

networks:
  agent-network:
    driver: bridge

生产环境覆盖配置

# docker-compose.prod.yml
version: '3.8'

services:
  agent:
    # 生产环境不重新构建,使用预构建镜像
    build: !reset null
    image: gcr.io/my-project/my-adk-agent:v1.2.3
    
    environment:
      - LOG_LEVEL=warn
      - LOG_FORMAT=json
      - MAX_CONCURRENT=500
      - METRICS_PORT=9090
    
    # 生产环境更严格的资源限制
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 1G
      replicas: 3  # Swarm 模式
    
    # 生产环境使用外部负载均衡,不直接暴露端口
    ports: !reset []

# 启动命令:
# docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

镜像安全:从构建到运行

镜像扫描

# 使用 Trivy 扫描漏洞
trivy image my-adk-agent:latest

# 只显示 HIGH 和 CRITICAL
trivy image --severity HIGH,CRITICAL my-adk-agent:latest

# 生成报告
trivy image --format json -o report.json my-adk-agent:latest

# 使用 Snyk
snyk container test my-adk-agent:latest

镜像签名(Cosign)

# 安装 cosign
# https://docs.sigstore.dev/cosign/installation

# 生成密钥对
cosign generate-key-pair

# 签名镜像
cosign sign --key cosign.key gcr.io/my-project/my-adk-agent:v1.2.3

# 验证签名
cosign verify --key cosign.pub gcr.io/my-project/my-adk-agent:v1.2.3

最小化攻击面

# 1. 使用非 root 用户
USER nonroot:nonroot

# 2. 只复制必需文件
COPY --from=builder /build/agent /agent

# 3. 不安装包管理器(distroless 天然满足)
# 4. 只暴露必要端口
EXPOSE 8080

# 5. 禁用 shell(distroless 没有 shell)
# 6. 使用只读文件系统(运行时配置)

常见问题深度排查

Q:镜像太大

诊断

# 查看镜像分层
docker history my-adk-agent:latest

# 分析镜像内容
docker run --rm -it my-adk-agent:latest ls -la /

# 使用 dive 工具分析
dive my-adk-agent:latest

优化策略

  1. 使用多阶段构建:只复制编译产物
  2. 选择最小基础镜像:distroless 或 alpine
  3. 清理缓存go mod download 后不需要保留源码
  4. 压缩层数:合并 RUN 指令
  5. 排除无关文件:使用 .dockerignore
# .dockerignore
.git
.gitignore
*.md
docker-compose*.yml
Dockerfile*
.env
.env.example
vendor/
dist/
*.test
*.out

Q:容器里找不到时区

根本原因:最小镜像(如 distroless、scratch)不包含时区数据。

解决方案

# 从 builder 复制时区数据
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# 设置默认时区(可选)
ENV TZ=Asia/Shanghai

Q:环境变量没传进去

诊断

# 查看容器环境变量
docker exec my-agent env

# 查看应用实际读取的配置
docker exec my-agent /agent --dump-config

常见原因

  1. docker run -e 语法错误
  2. 应用使用 .env 文件覆盖了环境变量
  3. 环境变量名大小写不匹配

最佳实践

# docker-compose.yml
services:
  agent:
    env_file:
      - .env  # 基础配置
    environment:
      - GOOGLE_API_KEY=${GOOGLE_API_KEY}  # 从宿主机环境变量传入
# .env 文件(不提交到 Git)
LOG_LEVEL=info
MAX_CONCURRENT=200

# 敏感信息通过宿主机环境变量传入
# export GOOGLE_API_KEY=xxx

下一步

Docker 部署搞定了,接下来看云端部署——Cloud Run 和 GKE。

Web 界面部署 | Cloud Run / GKE 部署 →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。