Data Engineering Zoomcamp:用 Docker 容器化 Python 数据管道(pip 与 uv 双方案实战)
【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp
导读
本篇指南是 Data Engineering Zoomcamp 2027 模块 01「Containerization and Infrastructure as Code」的第三个单元,讲解如何把上一单元中编写的 Python 数据管道脚本容器化。你将学会编写两种Dockerfile(基于pip与基于uv),掌握docker build/docker run的核心用法与参数传递方式,并结合仓库中真实的数据摄取脚本(ingest_data.py)理解容器化在「CSV → 数据管道 → PostgreSQL」场景下的实际落地方式。读完本文,你可以独立把一个 pandas 管道打包成可复用的 Docker 镜像。
1. 为什么要把数据管道容器化
在前一单元 02-virtual-environment.md 中,我们创建了一个pipeline目录,并在其中编写了pipeline.py:脚本接收命令行参数(如日期day),用 pandas 构造 DataFrame 并输出为 Parquet 文件。为了管理 pandas、pyarrow 等依赖,我们引入了uv虚拟环境来隔离项目依赖。
但虚拟环境只解决了「本机依赖隔离」的问题。当管道需要部署到其他机器、云端或 CI 环境时,仍然会遇到:
- 目标机器上缺少 Python 与依赖库,需要重复安装;
- 不同环境间 Python 版本、依赖版本不一致导致「在我机器上能跑」;
- 每次部署都要手工执行安装步骤,难以保证可复现。
Docker 的解决方案是把「代码 + 运行时 + 依赖」一起打包进一个不可变镜像,让管道在任何装有 Docker 的机器上以完全一致的方式运行。这正是本单元 03-dockerizing-pipeline.md 要完成的事。
2. 方案一:基于 pip 的简单 Dockerfile
文档首先给出了一个最直观的容器化方案——直接在基础镜像中用pip安装依赖:
# base Docker image that we will build on FROM python:3.13.11-slim # set up our image by installing prerequisites; pandas in this case RUN pip install pandas pyarrow # set up the working directory inside the container WORKDIR /app # copy the script to the container. 1st name is source file, 2nd is destination COPY pipeline.py pipeline.py # define what to do first when the container runs # in this example, we will just run the script ENTRYPOINT ["python", "pipeline.py"]2.1 指令逐行解析
| 指令 | 作用 | 说明 |
|---|---|---|
FROM | 指定基础镜像 | 这里使用官方python:3.13.11-slim,slim 变体体积更小,适合作为运行管道的基础 |
RUN | 在构建期间执行命令 | pip install pandas pyarrow一次性安装管道所需依赖 |
WORKDIR | 设置容器内工作目录 | 后续COPY、ENTRYPOINT都相对于该目录执行(此处为/app) |
COPY | 将文件复制进镜像 | 第一个参数是宿主机源文件,第二个参数是容器内目标路径 |
ENTRYPOINT | 定义容器启动时执行的默认命令 | 此处直接运行python pipeline.py |
2.2 构建与运行
假设pipeline.py与Dockerfile位于同一目录,并在该目录下执行构建:
docker build -t test:pandas .- 镜像名(repository)为
test,标签(tag)为pandas; - 若省略标签,Docker 会默认使用
latest; - 命令末尾的
.表示构建上下文(build context)为当前目录。
运行容器并传入参数,使管道脚本通过sys.argv接收到它:
docker run -it test:pandas some_number运行后你应看到与直接执行python pipeline.py some_number完全一致的输出——这正是容器化的意义:同样的代码、同样的行为。-it表示以交互模式挂接终端,便于观察管道日志输出。
注意:以上说明假定
pipeline.py与Dockerfile在同一目录,且 Docker 命令也在该目录下执行,否则构建上下文将无法找到源文件。
2.3 pip 方案的局限
pip 方案足够简单,但存在两个工程隐患:
- 版本不可复现:
pip install pandas pyarrow每次构建都拉取当时的最新版本,依赖漂移会导致镜像内容随时间变化; - 构建慢、缓存差:代码与依赖安装混在一起,改动一行代码就要重新解析依赖。
这正是文档随后引入uv方案的原因。
3. 方案二:基于 uv 的 Dockerfile(推荐)
uv是前序单元中已经使用的现代 Python 包管理器(用 Rust 编写,由 02-virtual-environment.md 介绍),本单元把它带入容器构建流程:
# Start with slim Python 3.13 image FROM python:3.13.10-slim # Copy uv binary from official uv image (multi-stage build pattern) COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/ # Set working directory WORKDIR /app # Add virtual environment to PATH so we can use installed packages ENV PATH="/app/.venv/bin:$PATH" # Copy dependency files first (better layer caching) COPY "pyproject.toml" "uv.lock" ".python-version" ./ # Install dependencies from lock file (ensures reproducible builds) RUN uv sync --locked # Copy application code COPY pipeline.py pipeline.py # Set entry point ENTRYPOINT ["uv", "run", "python", "pipeline.py"]3.1 与 pip 方案的关键差异
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/:直接从官方 uv 镜像把二进制复制进当前镜像,属于多阶段构建(multi-stage build)模式,无需在容器内再用pip install uv;- 先复制依赖清单、再复制代码:
pyproject.toml、uv.lock、.python-version先进入镜像并执行RUN uv sync --locked。由于 Docker 按层缓存,只要依赖文件没变,这层缓存就能被后续构建复用——改动代码时无需重新解析安装依赖; RUN uv sync --locked:严格按照锁文件uv.lock安装依赖,保证每次构建产出的环境完全一致(可复现构建);ENV PATH="/app/.venv/bin:$PATH":把 uv 创建的虚拟环境加入PATH,使容器内的python直接解析到虚拟环境中的解释器;ENTRYPOINT ["uv", "run", "python", "pipeline.py"]:通过uv run启动脚本,它会自动确认虚拟环境已就绪。
3.2 为什么依赖清单里要有这三个文件
| 文件 | 作用 |
|---|---|
pyproject.toml | 项目声明文件,记录元数据与直接依赖及其版本约束 |
uv.lock | 锁定全部依赖(含传递依赖)的精确版本,是可复现构建的基石 |
.python-version | 声明项目要求的 Python 版本,uv 会据此解析解释器 |
在仓库的 pipeline/pyproject.toml 中可以看到,真实项目的依赖不止 pandas 与 pyarrow,还包括click(命令行参数解析)、psycopg2-binary(PostgreSQL 驱动)、sqlalchemy(写库 ORM)、tqdm(进度条),以及 dev 依赖组jupyter、pgcli。这种「依赖清单驱动构建」的方式,正是 uv 方案面向生产可维护性的设计。
4. 仓库中的真实 Dockerfile:从 pipeline.py 到 ingest_data.py
文档中的pipeline.py是教学用的最小示例。在课程后续单元中,这个管道会演进为真正对接 PostgreSQL 的数据摄取脚本ingest_data.py(见 06-ingestion-script.md),其容器化 Dockerfile 位于 pipeline/Dockerfile:
FROM python:3.13.11-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/ WORKDIR /code ENV PATH="/code/.venv/bin:$PATH" COPY pyproject.toml .python-version uv.lock ./ RUN uv sync --locked COPY ingest_data.py . ENTRYPOINT ["python", "ingest_data.py"]4.1 与教学版 Dockerfile 的差异
WORKDIR /code替代/app,目录命名更贴近「代码运行目录」的语义;- 依赖清单复制与
uv sync --locked的顺序保持一致,同样利用层缓存; COPY ingest_data.py .只复制单个入口脚本,镜像更精简;- 入口为
python ingest_data.py(注意此处未再显式加uv run,因为PATH已指向虚拟环境)。
4.2 从最小示例到生产管道的思路迁移
ingest_data.py使用click定义了一组命令行选项(--pg-user、--pg-pass、--pg-host、--pg-port、--pg-db、--year、--month、--target-table、--chunksize),以 10 万行/块的方式流式读取 NYC 出租车数据并写入 PostgreSQL。容器化后,构建与运行方式为:
cd pipeline docker build -t taxi_ingest:v001 . docker run -it \ --network=pg-network \ taxi_ingest:v001 \ --pg-user=root \ --pg-pass=root \ --pg-host=pgdatabase \ --pg-port=5432 \ --pg-db=ny_taxi \ --target-table=yellow_taxi_trips这里有两个关键点(详见 08-dockerizing-ingestion.md):
--network=pg-network必须放在镜像名之前,用于让摄取容器找到另一个网络中的 PostgreSQL 容器;--pg-host不再指向localhost,而是指向 PostgreSQL 容器的服务名/容器名(pgdatabase)。
5. 容器化管道的完整工作流:结合 Docker Compose
在实际课程练习中,PostgreSQL 与 pgAdmin 往往通过 docker-compose.yaml 一键拉起(详见 09-docker-compose.md):
services: pgdatabase: image: postgres:18 environment: POSTGRES_USER: "root" POSTGRES_PASSWORD: "root" POSTGRES_DB: "ny_taxi" volumes: - ny_taxi_postgres_data:/var/lib/postgresql ports: - "5432:5432" pgadmin: image: dpage/pgadmin4 environment: PGADMIN_DEFAULT_EMAIL: "admin@admin.com" PGADMIN_DEFAULT_PASSWORD: "root" volumes: - pgadmin_data:/var/lib/pgadmin ports: - "8085:80" volumes: ny_taxi_postgres_data: pgadmin_data:启动服务:
docker-compose up # 前台运行 docker-compose up -d # 后台(detached)运行 docker-compose down # 停止并移除容器 docker-compose down -v # 停止并连同数据卷一并移除 docker-compose logs # 查看日志当 PostgreSQL 由 compose 管理时,摄取容器需加入 compose 自动创建的网络(名称通常形如pipeline_default,取决于目录名),运行:
docker network ls docker run -it --rm \ --network=pipeline_default \ taxi_ingest:v001 \ --pg-user=root \ --pg-pass=root \ --pg-host=pgdatabase \ --pg-port=5432 \ --pg-db=ny_taxi \ --target-table=yellow_taxi_trips至此,**管道代码(Docker 镜像)与存储服务(compose 网络)**被清晰地解耦:镜像只管「读数据、写数据」,服务编排只负责「把数据库和监控工具跑起来」,两者通过 Docker 网络按名称互相发现。
6. 总结与实践要点
| 对比维度 | pip 方案 | uv 方案 |
|---|---|---|
| 依赖管理 | pip install,版本不锁定 | uv sync --locked,锁文件保证可复现 |
| 构建缓存 | 依赖与代码同层,缓存收益低 | 先拷依赖清单再拷代码,代码改动命中缓存 |
| 虚拟环境 | 无显式隔离 | uv 自动管理.venv并写入PATH |
| 适用场景 | 快速原型、演示 | 面向生产、长期维护的管道 |
动手实践建议按以下顺序验证:
- 参照 02-virtual-environment.md 编写
pipeline.py并用uv初始化项目(uv init --python=3.13、uv add pandas pyarrow); - 依次使用 pip 与 uv 两个 Dockerfile 构建镜像(
docker build -t test:pandas ./docker build -t test:uv .),对比镜像构建速度与输出; - 用
docker run -it test:pandas 10传入参数验证sys.argv传递; - 进阶练习:把仓库中的 pipeline/Dockerfile 与 pipeline/ingest_data.py 结合 compose 跑通「下载 → 清洗 → 入 PostgreSQL」的完整链路。
容器化的收益在后续所有模块中都会反复体现:无论后续是 Spark 批处理、Flink 流处理还是 dbt 转换,凡是涉及 Python 依赖的环节,都可以复用本文的uv + 锁文件 + 层缓存模式,确保可复现、可迁移、可交付。
【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考