Docker 容器化部署:一次构建,到处运行
详解如何将 ADK Go Agent 打包成 Docker 镜像——多阶段构建、镜像优化、生产镜像配置。
Docker 容器化部署:一次构建,到处运行
Docker 容器化是现代应用部署的事实标准。对于 Go 应用而言,容器化的优势尤为明显:静态编译的二进制文件可以运行在极小的基础镜像中,最终镜像体积可以控制在 20MB 以内。但容器化不只是 docker build 和 docker 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 影响宿主机 |
--cpus | CPU 限制 | 防止 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
优化策略:
- 使用多阶段构建:只复制编译产物
- 选择最小基础镜像:distroless 或 alpine
- 清理缓存:
go mod download后不需要保留源码 - 压缩层数:合并 RUN 指令
- 排除无关文件:使用
.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
常见原因:
docker run -e语法错误- 应用使用
.env文件覆盖了环境变量 - 环境变量名大小写不匹配
最佳实践:
# 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 编程实战干货。
