第02章 容器化:Docker打包你的Agent

第02章 容器化:Docker打包你的Agent

“Build once, run anywhere — 但前提是你的 Dockerfile 写对了。” —— 容器化工程实践


Docker 是 Agent 生产化的起点。它解决了"在我机器上能跑"的经典问题,让你的 Agent 在开发、测试、生产环境中行为完全一致。但一个草率的 Dockerfile 会带来镜像过大、安全漏洞、启动慢等问题。本章告诉你怎么写对。


2.1 Dockerfile 最佳实践

# ====================================================
# Agent 生产级 Dockerfile(多阶段构建)
# ====================================================

# 阶段1:构建阶段(安装依赖)
# 使用 slim 版本减少基础镜像大小
FROM python:3.11-slim AS builder

# 安装构建依赖(只在构建阶段需要)
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /build

# 先复制依赖文件(利用层缓存:依赖不变时不重新安装)
COPY requirements.txt .

# 安装依赖到独立目录(方便复制到下一阶段)
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt


# 阶段2:运行阶段(最终镜像)
FROM python:3.11-slim AS runner

# 安全实践:不以 root 用户运行
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# 从构建阶段复制安装好的依赖
COPY --from=builder /install /usr/local

# 复制应用代码
COPY --chown=appuser:appuser app/ ./app/

# 切换到非 root 用户
USER appuser

# 健康检查(Kubernetes/Docker 会用这个判断容器是否健康)
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"

# 暴露端口
EXPOSE 8000

# 启动命令:使用 uvicorn 而非 python main.py
# --workers 1:容器内单进程,由 K8s 负责水平扩展
# --host 0.0.0.0:允许容器外访问
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
# ====================================================
# Python 代码:Dockerfile 分析和建议
# ====================================================

"""
常见 Dockerfile 反模式和修复方案:

❌ 反模式1:把所有东西装进一个阶段
    FROM python:3.11
    RUN pip install ... && apt-get install gcc ...
    COPY . .
    → 镜像包含了 gcc、临时文件等不必要的东西,轻松 1GB+

✅ 修复:多阶段构建(如上所示)
    构建阶段安装依赖,运行阶段只复制必要文件
    → 镜像从 1.2GB 缩到 200-400MB

❌ 反模式2:COPY . . 放在依赖安装之前
    COPY . .
    RUN pip install -r requirements.txt
    → 任何代码变更都会让 pip install 重新执行

✅ 修复:先 COPY requirements.txt,再 COPY 代码
    利用 Docker 层缓存:依赖不变时,pip install 直接用缓存

❌ 反模式3:以 root 用户运行
    (没有 USER 指令,默认是 root)
    → 如果应用被入侵,攻击者拿到 root 权限

✅ 修复:创建非特权用户并切换

❌ 反模式4:把密钥写进镜像
    ENV OPENAI_API_KEY=sk-xxxxx
    → 密钥会保存在镜像层里,docker inspect 就能看到

✅ 修复:密钥通过运行时环境变量注入(第04章详细讲)
"""

import subprocess
import json
from dataclasses import dataclass
from typing import List, Tuple


@dataclass
class DockerImageMetrics:
    """Docker 镜像指标"""
    image_name: str
    size_mb: float
    layers: int
    has_root_user: bool
    has_healthcheck: bool


def analyze_dockerfile(content: str) -> List[str]:
    """
    分析 Dockerfile,返回改进建议
    这是简化版,实际可以用 hadolint 等工具
    """
    suggestions = []
    lines = content.lower()
    
    if "from python" in lines and "slim" not in lines and "alpine" not in lines:
        suggestions.append("⚠️ 建议使用 python:x.y-slim 替代完整镜像,可减少 60% 镜像大小")
    
    if "as builder" not in lines:
        suggestions.append("⚠️ 建议使用多阶段构建(添加 AS builder 阶段)")
    
    if "user " not in lines:
        suggestions.append("⚠️ 添加非 root 用户(安全实践): RUN useradd -r appuser && USER appuser")
    
    if "healthcheck" not in lines:
        suggestions.append("⚠️ 添加 HEALTHCHECK 指令,用于容器编排的健康探测")
    
    if "copy . ." in lines:
        # 检查顺序
        copy_all_pos = lines.find("copy . .")
        requirements_pos = lines.find("requirements")
        if requirements_pos > copy_all_pos:
            suggestions.append("⚠️ COPY requirements.txt 应该在 COPY . . 之前,以利用层缓存")
    
    if "--no-cache-dir" not in lines and "pip install" in lines:
        suggestions.append("💡 pip install 添加 --no-cache-dir 标志,减少镜像大小")
    
    if not suggestions:
        suggestions.append("✅ Dockerfile 看起来不错!")
    
    return suggestions


# 示例:分析一个有问题的 Dockerfile
bad_dockerfile = """
FROM python:3.11
COPY . .
RUN pip install -r requirements.txt
CMD python app.py
"""

print("分析 Dockerfile 问题:")
for suggestion in analyze_dockerfile(bad_dockerfile):
    print(f"  {suggestion}")

2.2 .dockerignore 和镜像优化

# ====================================================
# 镜像优化:.dockerignore 和层缓存
# ====================================================

"""
.dockerignore 内容(放在项目根目录):

.git
.gitignore
__pycache__
*.pyc
*.pyo
.pytest_cache
.venv
venv
env
.env
.env.*       # 环境变量文件!不能进镜像
tests/       # 测试文件不需要在生产镜像里
docs/
*.md
Makefile
docker-compose*.yml
.DS_Store

【为什么要排除 .env 文件?】
如果 .env 文件进了 Docker 镜像,那么:
1. 所有能拉取这个镜像的人都能看到你的密钥
2. 镜像层是持久化的,即使删除 .env 后重新构建,
   如果你重用了之前的层,旧版本中的密钥依然在镜像历史里

正确做法:密钥通过运行时注入(第04章),不进镜像。
"""


def generate_dockerignore() -> str:
    """生成标准的 .dockerignore 内容"""
    return """\
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
.venv/
venv/
env/

# 测试和开发工具
tests/
.pytest_cache/
.coverage
htmlcov/
.mypy_cache/
.ruff_cache/

# 密钥和配置(绝不进镜像)
.env
.env.*
*.secret
secrets/

# 版本控制
.git/
.gitignore

# 文档和元数据
*.md
docs/
Makefile

# IDE
.vscode/
.idea/
*.swp

# OS
.DS_Store
Thumbs.db
"""


# 演示:构建参数说明
BUILD_COMMANDS = {
    "开发构建": "docker build -t my-agent:dev .",
    "生产构建": "docker build --target runner -t my-agent:v1.0.0 .",
    "查看镜像大小": "docker images my-agent",
    "查看镜像层": "docker history my-agent:v1.0.0",
    "安全扫描": "docker scout cves my-agent:v1.0.0",  # Docker Scout
    "运行容器": (
        "docker run -p 8000:8000 "
        "-e OPENAI_API_KEY=$OPENAI_API_KEY "  # 运行时注入密钥
        "--name my-agent "
        "my-agent:v1.0.0"
    ),
}

print(".dockerignore 模板:")
print(generate_dockerignore())

print("\n常用 Docker 命令:")
for purpose, cmd in BUILD_COMMANDS.items():
    print(f"  [{purpose}] {cmd}")

本章小结

  1. 多阶段构建是生产 Dockerfile 的标配:构建阶段装编译工具和安装依赖,运行阶段只有运行时需要的文件。镜像大小从 1GB+ 降到 200-400MB,攻击面也大幅减少。

  2. COPY 的顺序决定缓存的效率:先 COPY requirements.txt 并安装依赖,再 COPY 代码。这样代码变更时不会触发重新安装依赖,CI/CD 构建时间从分钟级降到秒级。

  3. 不以 root 用户运行容器:这是安全最佳实践,也是许多企业的强制要求。一行 USER appuser 可以显著降低被入侵时的影响范围。

  4. .env 文件绝对不能进 Docker 镜像:即使 Dockerfile 里看似删除了,Docker 层历史里仍然保留。密钥只能通过运行时环境变量注入,永远不要在构建时写入镜像。

  5. HEALTHCHECK 是容器编排的必要配置:没有 HEALTHCHECK,Kubernetes/Docker Compose 不知道你的容器是否真的"健康"——进程活着不等于服务可用。

# 核心行动:用这个模板重写你的 Dockerfile
# 三步完成容器化改造:
# 1. 创建 Dockerfile(用本章多阶段构建模板)
# 2. 创建 .dockerignore(用本章模板)
# 3. 测试:docker build + docker run,验证功能正常

# 快速检验:
# docker image ls | grep your-agent       # 查看镜像大小(目标<500MB)
# docker inspect --format='{{.Config.User}}' your-agent  # 应该是 appuser,不是 root

本章提示词模板

【模板1:Dockerfile 生成提示词】
我需要为以下 Python 服务生成一个生产级的 Dockerfile:

服务类型:{service_type}(如 FastAPI Web 服务 / 定时任务 / 消息消费者)
Python 版本:{python_version}
依赖描述:{dependencies_description}
特殊要求:{special_requirements}(如需要系统库 / GPU 支持 / 特定文件权限)

请生成:
1. 多阶段构建的 Dockerfile(构建阶段 + 运行阶段)
2. 对应的 .dockerignore 文件
3. 启动命令(CMD)的建议(包括 workers 数量的建议)
4. 如果有特殊依赖需要系统包,请说明安装方法
5. 列出这个 Dockerfile 中做了哪些安全加固
【模板2:镜像优化诊断提示词】
我的 Docker 镜像有以下问题需要优化:

当前镜像大小:{image_size}
构建时间:{build_time}
已知问题:{known_issues}

Dockerfile 内容:
{dockerfile_content}

请帮我:
1. 找出导致镜像过大的主要原因
2. 提供具体的优化方案(改哪些行,怎么改)
3. 预估优化后的镜像大小
4. 有没有潜在的安全问题需要修复?
5. 如何验证优化后的镜像功能与之前完全一致?

→ 第03章:API服务化:FastAPI生产级设计