六段骨架:目标 → 环境准备 → 步骤 → 验证 → 设计权衡 → 踩坑记录 → 下一步

目标

把第 09 / 11 篇写好的 Agent 脚本,打包成一个 Docker 镜像:别人(或服务器)只需要有 Docker,一条 docker run 就能跑起来,不用关心他机器上有没有 Python、版本对不对、依赖装没装。

一句话价值:消灭"在我机器上能跑"这个永恒借口。

背景衔接

前面几篇你在本机跑通了三种 Agent:

  • 09 篇:手写的 Tool Calling 循环(agent.py
  • 10 篇:把工具标准化成 MCP Server
  • 11 篇:用 LangGraph 把流程画成图

它们现在都躺在你的项目目录里。本篇用 Docker 给这个项目"拍个快照",让它变成可复制、可分发、可上线的形态。

环境准备

  • 第 01 篇已经装好 Docker(Windows 上的 Docker Desktop,或 Linux 上的 Docker Engine)
  • 一个能跑的 Agent 项目(以第 11 篇的 LangGraph 项目为例,目录结构如下)
  • 模型 Key 放在 .env 里(第 09 篇就是这么做的)

假设你的项目长这样:

my-agent/
├── app.py            # LangGraph 主程序(第 11 篇的 graph 代码)
├── requirements.txt  # 依赖清单
├── .env              # OPENAI_API_KEY / BASE_URL / MODEL(密钥,不进镜像)
├── Dockerfile        # 本篇要写的
├── .dockerignore     # 本篇要写的
└── docker-compose.yml# 可选,编排用

requirements.txt 内容(和本机开发时一致,不要偷懒写 *):

openai
langgraph
python-dotenv

步骤

① 写 Dockerfile

my-agent/ 下新建 Dockerfile(无扩展名):

# 用确定版本的基础镜像,别用 latest(latest 下次构建会变,破坏可复现)
FROM python:3.11-slim

# 统一工作目录,后面所有相对路径都基于它
WORKDIR /app

# 先只拷依赖清单并安装——这层会被 Docker 缓存,
# 只要你不动 requirements.txt,改业务代码就不用重装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再拷全部代码(包含 .env 之外的源码)
COPY . .

# 容器启动时执行的命令
CMD ["python", "app.py"]

app.py 里读 Key 的方式保持第 09 篇的写法即可(用 python-dotenv.env):

from dotenv import load_dotenv
import os
load_dotenv()  # 运行时从 .env 读,不写死在代码里
API_KEY = os.getenv("OPENAI_API_KEY")

② 写 .dockerignore(防密钥进镜像,关键)

作用像 .gitignore,告诉 Docker 构建时哪些文件别打进镜像。最重要的是排除 .env

.env
venv
.venv
__pycache__
*.pyc
.git
.gitignore

⚠️ 这步是安全红线:如果忘了写 .dockerignoreCOPY . . 会把 .env(含你的模型 Key)一起打进镜像层。任何人拿到镜像都能 docker history 看到,或反解出 Key。

③ 构建镜像

在项目目录里执行:

docker build -t my-agent:0.1 .
  • my-agent:0.1 是镜像名和标签,0.1 方便以后版本管理
  • 末尾的 . 表示用当前目录的 Dockerfile

构建成功会看到 Successfully tagged my-agent:0.1

④ 运行容器(密钥运行时注入,不打进镜像)

docker run --rm --env-file .env my-agent:0.1
  • --env-file .env:把本机的 .env 注入容器环境变量,Key 始终只在你本地,不进镜像
  • --rm:容器退出后自动清理,不留垃圾

如果 app.py 需要持续运行(比如是个服务),加 -d 后台运行,用 docker logs 看输出:

docker run -d --name my-agent --env-file .env my-agent:0.1
docker logs -f my-agent

⑤ (可选)用 docker-compose 编排

当 Agent 要搭配其他服务(比如一个前端、一个数据库)时,写 docker-compose.yml 更省事:

services:
  agent:
    build: .
    env_file: .env          # 同样运行时注入,不进镜像
    restart: unless-stopped # 意外退出自动拉起

启动:docker compose up -d --build

验证

三项全过 = 部署成立:

  1. docker images 能看到 my-agent:0.1(说明构建成功)
  2. docker run --rm --env-file .env my-agent:0.1 输出正确结果(问时间能调工具、问笑话能直接答,和第 11 篇本地跑一致)
  3. 改一行 app.py(比如打印一句 "v2"),重新 docker buildrun,输出变化(说明镜像确实是最新代码,没缓存旧版)

第 2 项最关键:它证明"换一台只有 Docker 的机器,也能原样跑通"。

设计权衡:Docker vs venv vs 裸跑

维度裸跑(直接 python app.py)venv 虚拟环境Docker 容器
环境一致性最差,依赖本机较好,但 Python 版本仍依赖本机最好,连系统层都隔离
上手成本0中(要学 Dockerfile)
分发他人难,"你装下依赖"较难易,给镜像或 Dockerfile 即可
部署上线几乎不可行勉强标准做法(云厂商原生支持)
资源开销0极低多一层隔离,略重
适用阶段本地调试个人开发交付 / 部署 / 协作

诚实地说:单机自娱自乐,venv 足够;要给别人用、要上线,才值得上 Docker。这点和你"反过度工程"的判断一致——别在 demo 阶段就先写一整套 K8s。

踩坑记录

1. .env 被打进镜像层

忘了 .dockerignoreCOPY . . 把密钥带进去了。修复:echo ".env" >> .dockerignore重新 build(已有的镜像层不会自动消失,必须重建,必要时 docker build --no-cache)。

2. COPY 顺序写反,依赖每次都重装

写成 COPY . .COPY requirements.txt .pip install,导致改任何代码都会让依赖层缓存失效、全量重装。正确顺序见步骤 ①:先 requirements.txt 后源码。

3. requirements 没锁版本

只写 langgraph 不写版本,三个月后别人 build 拿到新版 API,代码跑挂。演示项目可写 langgraph==0.x,生产项目用 pip freeze > requirements.txt 锁死。

4. Windows 路径挂载权限

WSL2 下把 Windows 目录挂进容器(-v /mnt/d/...:/app)可能遇到权限或换行符(CRLF)问题。尽量在 WSL 文件系统内构建,或确保 .dockerignore 排除无关文件。

5. 暴露了不必要的端口

Agent 是脚本不是服务,别乱写 EXPOSE 8080-p 映射,反而增加攻击面。只有真正起服务时才开端口。

6. 镜像体积爆炸

python:latest(带编译器等,1G+)且 pip install 不带 --no-cache-dir,镜像臃肿、上传慢。改用 python:3.11-slim + --no-cache-dir,体积能砍到一两百兆。

下一步

镜像打好了,Agent 变成"可分发"的形态。下一步是给它找个——发到 GitHub,让它能作为你的公开作品被看到、被复用:

→ AI Agent 开发实战(13):GitHub 开源

第 13 篇把项目推上 GitHub,补全 README / LICENSE / .gitignore,成为你简历里能点开的作品链接。


系列衔接:09(手写 Agent)→ 10(工具标准化)→ 11(流程编排)→ 12(容器化部署)→ 13(开源交付),工程化这条线走到"能交付给别人"。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐