AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

Docker 与可复现环境:代码一样,为什么换台机器就跑不起来?

我的学习 →

正在读取学习记录…

Docker 帮我们把程序和运行材料装进可重复构建的镜像,再用镜像启动容器。Dockerfile 是打包配方,镜像是按配方装好的程序和依赖,容器是用这份镜像启动的一次运行。上一章的 Git 提交回答“是哪版代码”,这里进一步回答“代码用什么环境执行”。

先用一段不依赖第三方包的 Python 程序构建镜像,在容器里看到正常输出。之后再看相同提交为何可能在另一台电脑失败,以及配置、数据和卷要如何处理。Agent 的检索程序、模型客户端与工具服务也面对同样的交付问题。

读完后,你应能区分镜像与容器,预测容器内新写的文件会怎样,并判断“构建成功”之后还要检查什么。命名卷部分再用两个容器验证数据去向。

首次构建会下载基础镜像,先确认网络、磁盘空间和组织的软件使用要求。下文会分别练习构建、运行和用命名卷保存数据。

进一步理解:虚拟环境、容器和虚拟机有什么区别

从“我的环境”拆出五件事

遇到“我这里能跑”,先对比双方的运行条件:

条件具体问题应保留的记录
源码是哪次提交?是否有未提交修改?提交号与工作区状态
解释器和系统库Python 是哪个版本?本地库是否可用?运行时与基础环境
第三方依赖同名库的版本是否一致?依赖声明与锁定结果
配置输入路径、时区、服务端点是否不同?无秘密的配置说明
输入与外部服务资料是否更新?远端模型是否变化?输入版本及服务信息

Docker 主要封装程序和依赖。外部数据库、模型接口和运行输入仍分别管理,排查差异时一起对照这几项。

虚拟环境、容器、虚拟机怎样选

Python 的 venv 适合把项目依赖分开;它通常仍依赖当前机器的解释器和系统环境。Linux 容器把进程放在隔离的运行环境中,并使用镜像提供文件系统内容;同一 Linux 内核上的容器共享内核。虚拟机则可以运行自己的客体内核。Windows 上运行 Linux 容器还涉及 Linux 运行环境,不能把容器理解为直接执行 Windows 上的普通进程。Docker 的容器概念说明

只做标准库练习时,venv加清楚的 README 可能足够。需要交付含系统库的工具、统一团队启动方式时,容器更有价值。需要不同内核或更强隔离方案时,要评估虚拟机等方案。容器有隔离机制,但不能因此把未知代码、宿主目录和高权限随意组合。

2. 镜像是交付物,容器是运行实例

先记住两个不同的动作:build 生成镜像,run 从镜像创建并运行容器。相同镜像可以创建多个容器,它们不是同一个进程,也不默认共用各自的可写文件。

文件和Dockerfile经构建形成镜像,同一镜像可以创建两个拥有各自可写层的容器,需保存的数据另外存放
图1:构建一次,得到镜像;同一镜像可以启动多个容器。

你可以把镜像想成准备好的运行材料,但它不是“当前正在执行的程序”。容器退出后,镜像仍可能存在;删除某个容器,也不等于删除构建它的源码。把这些对象混成一个“Docker”,排错时就很难说清究竟要检查哪一层。

再区分文件和进程:容器内主进程完成,容器会退出。打印结果就结束的脚本会正常退出。对于常驻服务,意外退出才需要结合退出码和日志调查。

3. 安装前先分清三个组件

组件作用Windows上容易混淆的地方
docker命令行客户端把命令发送给引擎能打印版本不等于能创建容器
Docker引擎构建镜像、创建和管理容器必须有实际运行的服务端
Docker Desktop桌面环境中的安装、管理和运行入口安装完成后还需要启动并等待引擎就绪

Windows上运行这里的Linux镜像需要相应Linux运行环境。WSL 2可以先理解为Windows提供的一种Linux运行方式;它不是这里要你另写的程序。先按 Docker Desktop Windows安装说明核对系统、硬件虚拟化与WSL要求,选择适合机器架构的官方安装包。工作电脑还应先核对组织许可和软件使用条款。

Windows 安装后从开始菜单启动 Docker Desktop,等待引擎就绪,再打开终端。

macOS 从 Docker Desktop for Mac 官方安装页选择 Apple 芯片或 Intel 芯片对应的安装包。打开下载的 .dmg,将 Docker 拖到 Applications,再从应用程序中启动 Docker,等待引擎就绪后打开终端。可以在苹果菜单的“关于本机”中查看芯片类型。

确认引擎可用,再谈应用错误

在终端执行:

Bash
docker version

这里要看客户端和服务端信息,而不是只看第一行版本号。客户端是你输入命令的工具,引擎负责实际创建容器。只有 Client 信息、随后出现无法连接引擎的错误,说明还不能构建或运行。此时改 Python 代码没有帮助。

只有 Client 信息、没有 Server 信息时,先打开 Docker Desktop,等待引擎启动,然后重跑 docker version。两部分都正常返回后,再开始构建。这样可以先排除“命令行已安装,但负责干活的引擎还没起来”这一类问题。

context 是客户端选择连接目标的一组设置,先用 docker context show 查看当前名称,再用 docker context ls 查看列表。看到陌生的远程目标时不要直接切换或运行实验;先确认它是不是你要使用的本机引擎。对于本例,Server部分能够正常返回,才继续第4节。仅凭桌面图标存在还不够。

4. 一个没有第三方依赖的最小交付

新建专门的练习目录,放入一个输出查询参数的 Python 程序和一份 Dockerfile。构建时 Docker 读取这个目录里的材料,因此只放这次需要的文件。

用资源管理器创建 agentdepth-docker 文件夹。在文本编辑器中保存下面两个文件,或者复制下方完整代码,保存为 app.py 与 Dockerfile;Dockerfile没有 .txt 后缀。

app.py:

Python
# 按固定输入观察Docker 与可复现环境的处理过程和返回结果。
import json
import platform

request = {"topic": "Agent 工程基础", "limit": 3}
if type(request["limit"]) is not int or request["limit"] <= 0:
    raise ValueError("limit 必须是正整数")
print(json.dumps({
    "python": platform.python_version(),
    "accepted": request,
}, ensure_ascii=False))

这段代码承接前面的输入校验:bool也是int的子类,所以此处使用精确类型判断排除布尔值。输出包含解释器版本,方便确认到底在哪里执行。运行后会打印查询参数与解释器版本。

同目录的 Dockerfile:

Dockerfile
FROM python:3.13.15-slim
WORKDIR /app
COPY app.py /app/app.py
USER 10001:10001
CMD ["python", "-X", "utf8", "/app/app.py"]

逐行看,而不是把它当咒语:FROM选择基础镜像;WORKDIR设置后续工作目录;COPY把选定文件纳入镜像;USER让这个只读脚本以非 root 身份运行;CMD给出默认执行命令。数组形式把参数分开,不依赖 shell 替你解析一整串文本。

这里的 Python 由 FROM 选定的基础镜像提供;CMD 直接用 python 运行程序。镜像标签与摘要的区别,放在第7节用实际命令查看。

在这两个文件所在目录执行:

先在终端用 cd 进入 agentdepth-docker 文件夹,再运行 ls 确认两个文件存在。下面的点 . 指当前目录。

Bash
docker build -t agentdepth-l0-env:lesson .

末尾的点是构建上下文路径;-t给镜像起名字。构建日志会逐步显示读取 Dockerfile、复制文件、导出镜像等步骤。构建正常结束、重新出现命令提示符后,运行容器:

Bash
docker run --rm --network none agentdepth-l0-env:lesson

构建成功后,容器打印一条 JSON 并正常退出:

JSON
{"python": "3.13.15", "accepted": {"topic": "Agent 工程基础", "limit": 3}}

--network none关闭容器的网络连接,--rm在脚本退出后删除这个容器;镜像和学习目录仍然保留。运行参数参考

python 显示容器里实际运行的解释器版本,accepted 显示通过检查的查询参数。把 limit 从3改成0,再构建并运行,程序会报告“limit 必须是正整数”;改回3后重新构建,恢复上面的输出。这样就能观察源码修改怎样进入新镜像。

5. 为什么 COPY 的范围值得单独检查

构建上下文是构建器可以读取的文件集合,不等于容器启动时的工作目录。COPY app.py从上下文读取文件,再写进镜像。你在错误目录执行 build,可能根本找不到它;把上下文设得过大,又可能把不该处理的文件带进去。构建上下文说明

很多教程为了省一行使用 COPY . .。对只有练习文件的目录可能方便,但真实项目里还有 .env、私人记录和缓存。更稳妥的习惯是先明确复制范围,再使用 .dockerignore排除无需进入构建上下文的内容。

TXT
.git
.venv
__pycache__
.env

注意 .gitignore与.dockerignore不是同一个文件:前者服务版本控制,后者服务构建上下文。前者排除了秘密,不代表 Docker 构建也自动排除。已经复制进某个镜像层的秘密,不能靠之后删除文件就当作从未存在。学习示例完全不需要密钥;后续接模型时再使用合适的运行时配置或秘密管理方式。

6. 文件消失,是保存错地方还是程序没写?

想象你把实验报告写到容器内的某个普通目录。只要该目录属于容器可写层,文件就跟这个容器绑定。停止和删除不是一回事:停止后重新启动同一个容器,文件可以仍在;删除容器后,不应期待再创建一个容器就自动找回它。

操作普通容器可写层独立命名卷
停止容器通常保留保留
删除容器随该容器删除不因普通容器删除自动消失
从同镜像创建新容器获得新的可写层显式挂载同一个卷才访问相同数据

命名卷由 Docker 管理,适合把数据生命周期与容器分开;绑定挂载则把指定宿主路径映射进去。它们都不是自动备份策略。挂载了错误的目录,或者让程序拥有写权限,仍可能改坏数据。卷的生命周期

停止容器时可写层仍随原容器保留;删除容器后可写层消失;新容器只有显式挂载同一个命名卷才可访问卷中数据
图2:沿“停止→删除→新建”读数据去向。普通可写层跟随容器;命名卷独立保留,新容器需显式挂载。

初学时先能解释这张表,再做保存报告的扩展。不要为了修复一个“权限不足”,马上改成 root、挂载整个磁盘或给容器特权。应先确认目标路径的属主、挂载方式,以及程序是否真的需要写那里。

自己验证:容器删了,报告还在吗?

前面的程序打印结果就退出了,现在把文字保存下来,观察新容器是否能读到。复制下方完整代码,保存为 write_volume.py 和 read_volume.py,保存在当前练习目录。前者创建 /data/evidence.txt 并写入文字,后者读取同一路径。Path 表示文件路径;open("x") 创建新文件,已有同名文件时会拒绝覆盖;with 在写完后关闭文件。

保存为 write_volume.py:

Python
# 将容器里的结果写入 /data;挂载命名卷时,后续容器还能读取这个文件。
from pathlib import Path

report = Path("/data/evidence.txt")
with report.open("x", encoding="utf-8") as stream:
    stream.write("AgentDepth volume lesson")
print(report.read_text(encoding="utf-8"))

保存为 read_volume.py:

Python
# 从同一挂载位置读取已有结果,检查数据是否跨容器保存。
from pathlib import Path

print(Path("/data/evidence.txt").read_text(encoding="utf-8"))

确认 Docker 引擎已启动,给新卷起一个没有用过的名字,然后逐行执行:

PowerShell
$volumeName = 'agentdepth-l0-my-first-volume'
$pythonImage = 'python:3.13.15-slim'
docker volume create $volumeName
docker run --rm --network none --mount "type=bind,src=$PWD,dst=/lesson,readonly" --mount "type=volume,src=$volumeName,dst=/data" $pythonImage python /lesson/write_volume.py
docker run --rm --network none --mount "type=bind,src=$PWD,dst=/lesson,readonly" --mount "type=volume,src=$volumeName,dst=/data" $pythonImage python /lesson/read_volume.py
docker volume inspect $volumeName --format '{{.Name}}'
macOS 终端命令
Bash
volumeName='agentdepth-l0-my-first-volume'
pythonImage='python:3.13.15-slim'
docker volume create $volumeName
docker run --rm --network none --mount "type=bind,src=$PWD,dst=/lesson,readonly" --mount "type=volume,src=$volumeName,dst=/data" $pythonImage python /lesson/write_volume.py
docker run --rm --network none --mount "type=bind,src=$PWD,dst=/lesson,readonly" --mount "type=volume,src=$volumeName,dst=/data" $pythonImage python /lesson/read_volume.py
docker volume inspect $volumeName --format '{{.Name}}'

预测:两次 docker run 都打印 AgentDepth volume lesson。每次结束时 --rm 会删掉那个容器,第二次创建的是新容器;它仍能读到文字,因为两次显式挂载了同一个命名卷。最后一行应打印你刚创建的卷名。若第二次找不到文件,先核对两条命令里的卷名是否完全相同,再查第一条是否成功写入。若第一次出现 FileExistsError,说明该卷里已有同名文件;open('x') 故意拒绝覆盖,此时不要直接删卷,先换一个全新的练习卷名。

两次挂载各有用途:bind 把当前目录的程序映射到容器里的 /lesson,readonly 让容器只能读取源码;volume 把持久数据映射到 /data。第二个容器能读到第一个写的文字,说明数据保存在卷中。这个卷由本机 Docker 引擎管理,换电脑需要另外迁移或恢复数据。确认练习卷没有需要保留的数据后,可执行 docker volume rm $volumeName;这会删除卷内的数据。

7. 固定版本为什么还不够

镜像标签像书名与版次,摘要(digest)像某一份内容的指纹。python:3.13.15-slim 选定 Python 版本与精简镜像系列,发布者仍可能更新其中的系统组件。要让同事取到完全相同的基础镜像,可以用摘要固定内容。Docker 的版本固定说明

交付程序时,记录这些运行材料:源码提交及未提交差异、基础镜像引用和实际摘要、构建参数、运行命令、输入文件版本,以及需要但不公开其秘密值的配置名称。

构建成功后,先查看基础镜像的仓库摘要和系统架构,再查看自己构建的镜像 ID:

Bash
docker image inspect python:3.13.15-slim --format '{{json .RepoDigests}}'
docker image inspect python:3.13.15-slim --format '{{.Os}}/{{.Architecture}}'
docker image inspect agentdepth-l0-env:lesson --format '{{.Id}}'

第一行返回一个列表,里面的引用形如 python@sha256:…;sha256: 后面是完整摘要。第二行显示镜像的操作系统和处理器架构。第三行的 Id 标识你在本机构建的产物。仓库摘要用于指定从仓库取得哪份内容,本地 ID 用于识别本机产物。镜像检查命令

进一步固定基础镜像时,把实际查询到的引用组成 Dockerfile 的第一行:

PowerShell
$baseImage = docker image inspect python:3.13.15-slim --format '{{index .RepoDigests 0}}'
"FROM $baseImage"
macOS 终端命令
Bash
baseImage=$(docker image inspect python:3.13.15-slim --format '{{index .RepoDigests 0}}')
printf 'FROM %s\n' "$baseImage"

复制输出的整行,替换原来的 FROM python:3.13.15-slim,其余行保持不变,再执行前面的构建与运行命令。之后构建会按这个摘要选择基础内容。若摘要列表为空,先执行 docker pull python:3.13.15-slim 再查询。更新基础镜像时,重新获取摘要、构建并测试,确认后保存新的引用。

后续 Agent 实验还要记录模型服务与评测输入。即使容器完全一致,外部接口更新、模型行为变化、时间相关输入也可能改变结果。环境复现帮助缩小排查范围,不把概率系统变成确定性函数。

8. 按失败阶段排查,比反复重建有效

现象先判断哪一层最小检查
无法连接 Docker 服务客户端与引擎连接docker version是否有 Server 信息
获取基础镜像失败镜像地址、网络或授权看失败发生在下载阶段还是应用阶段
COPY 找不到 app.py构建上下文当前目录、文件名、ignore规则
容器提示权限不足进程身份与路径程序写哪里、当前用户有何权限
脚本打印后容器退出进程生命周期本来是否就是一次性任务
新容器没有上次报告数据生命周期文件是否只写入已删除容器

故障记录保留执行命令、错误阶段、关键错误文本和预期行为,隐去身份信息与秘密。下一篇学习日志与测试时,我们会把这套人工判断变成可重复检查。

9. 自测:预测、解释,再迁移

课后练习展开习题收起习题
1. app.py改了,但没重新build,再次run会执行新代码吗?

如果 Dockerfile 使用 COPY 且没有绑定挂载,宿主文件修改不会自动进入已有镜像。需要重新构建,并确认运行的是新镜像。

2. 容器输出JSON后退出,是否说明Docker运行失败?

不一定。本例是一次性脚本,任务完成后退出符合预期。还应检查退出码和输出内容;单凭是否持续running无法判断业务成功。

3. .gitignore写了.env,就能放心COPY整个目录吗?

不能。Git忽略规则不等于Docker构建规则。应检查构建上下文、复制范围和.dockerignore,避免秘密进入镜像。

4. 同一个镜像运行两次,模型回答不同,该先重装Docker吗?

不应该。先对比输入、配置、外部服务和模型行为,再确认是否存在环境差异。镜像一致不保证所有外部条件相同。

做一个与程序直接对应的小变化:在 app.py 中把 "limit": 3 改为 "limit": 0,保存,再逐条执行:

PowerShell
docker build -t agentdepth-l0-env:lesson .
docker run --rm --network none agentdepth-l0-env:lesson
$LASTEXITCODE
macOS 终端命令
Bash
docker build -t agentdepth-l0-env:lesson .
docker run --rm --network none agentdepth-l0-env:lesson
echo $?

构建成功后,运行会在输入检查处抛出 ValueError,退出码为非零值。把 limit 恢复为3,重新执行同样三条命令,输出恢复为正常 JSON、退出码0。每次都重新构建,是因为 COPY 将当时的源码放入镜像;仅保存宿主文件不会改已有镜像。

完成这次修改后,核对三种结果:正常输入打印 JSON 并以 0 退出;非法输入出现校验错误并以非零值退出;恢复输入并重新构建后,正常结果回来。把每种结果对应到程序中的检查,就能理解镜像更新与程序运行的关系。

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.0

选择题 · build 和 run 分别做什么?

已经修改 app.py,但没有重新 build 镜像,直接 run 原镜像。最可能执行哪份代码?

对应知识点:L0-docker

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 为什么提交号还不够?

两台机器使用同一个 Git 提交号,但一台缺少系统库、另一台依赖版本不同。请写出还应记录的至少三类运行条件,并说明原因。

对应知识点:L0-docker

练习 3 / 4 · v1.0.0

代码题 · 收紧一个危险的构建上下文

把 `COPY . .` 改成只复制 app.py,并写出一个 .dockerignore,排除 .env、.git、.venv 和 __pycache__。提交文件片段与理由。

对应知识点:L0-docker

练习 4 / 4 · v1.0.0

场景题 · 容器删除后报告不见了

一次性容器生成了报告,运行时使用了 `--rm`,退出后报告消失。请设计保存方案,并说明为什么不能直接给容器 root 权限或挂载整个磁盘。

对应知识点:L0-docker

10. 把代码与运行环境连起来

对照刚才的操作:Dockerfile 描述配方,镜像保存构建成品,容器运行程序,数据卷保留需要跨容器保存的文件。

这一篇先记住一条主线:docker build 按 Dockerfile 把代码和依赖做成镜像,docker run 用镜像启动容器。改了电脑上的 app.py,已有镜像不会自己变;要重新构建。需要在容器结束后保留的数据,应另行设计卷或受限挂载。

跟做时,分别保存构建输出、容器运行结果和命名卷的读回记录。构建上下文与卷的细节可查 Docker 构建上下文 和 卷文档。

开源阅读先看Docker官方文档源码理解概念和示例,再在需要研究构建机制时看BuildKit,最后按问题进入Moby。这是学习顺序,不是活跃度或性能排名,新人不必现在读完底层源码。

前面的 Git 保存代码版本,Docker 组织运行条件。下一篇 日志、pytest 与 CI 把检查步骤变成可重复运行的测试。

参考资料

以下 Docker 官方资料可用于查阅安装步骤、镜像构建和容器命令。

读者问题官方资料这里用途
镜像、容器和运行命令各是什么?容器基础 · docker run图1与一次性任务的预期行为
Windows 如何安装并判断引擎就绪?Docker Desktop Windows 安装 · docker version第3节的安装与引擎检查
COPY 能读哪些文件、怎样排除?构建上下文与 .dockerignore第5节的边界与秘密风险
删除容器后数据在哪里?卷与容器可写层第6节表格和图2
标签与摘要如何影响复现?构建最佳实践 · docker image inspect第7节的版本定位

命令结果对应前面的构建、运行和数据卷操作。遇到问题时,先确认失败发生在引擎连接、镜像下载、构建还是程序运行,再查看对应输出。

图片查看器
100%

文章配图 · 按 Esc 关闭