Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile
文章目录
- [Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile](#Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile)
-
- 一、先看完整目录结构
- 二、为什么这次必须把源码和数据文件都列出来
- 三、这个案例到底在做什么
- 四、先把项目文件全部写出来
-
- [1. `requirements.txt`](#1.
requirements.txt) - [2. `data/sample.csv`](#2.
data/sample.csv) - [3. `src/main.py`](#3.
src/main.py) - [4. `Dockerfile`](#4.
Dockerfile)
- [1. `requirements.txt`](#1.
- [五、怎么理解这份 Dockerfile](#五、怎么理解这份 Dockerfile)
-
- [1. `FROM python:3.11-slim`](#1.
FROM python:3.11-slim) - [2. `WORKDIR /app`](#2.
WORKDIR /app) - [3. `COPY requirements.txt .`](#3.
COPY requirements.txt .) - [4. `RUN pip install --no-cache-dir -r requirements.txt`](#4.
RUN pip install --no-cache-dir -r requirements.txt) - [5. `COPY src ./src`](#5.
COPY src ./src) - [6. `COPY data ./data`](#6.
COPY data ./data) - [7. `COPY output ./output`](#7.
COPY output ./output) - [8. `CMD ["python", "src/main.py"]`](#8.
CMD ["python", "src/main.py"])
- [1. `FROM python:3.11-slim`](#1.
- 六、如何一步步复现这个项目
- 七、第一次运行:不挂载本地目录
- 八、第二次运行:把输出目录挂载回本地
- 九、为什么挂载后,容器里的目录会"变成"本地目录
- [十、如果把本地数据目录挂载到 `/app/data`](#十、如果把本地数据目录挂载到
/app/data) - 十一、这个案例真正串起了哪些知识
- 十二、这一部分最容易混淆的点
-
- [1. `RUN` 和 `CMD` 不是一回事](#1.
RUN和CMD不是一回事) - [2. `COPY` 和 `-v` 不是一回事](#2.
COPY和-v不是一回事) - [3. `/app` 和 `src/` 不是一回事](#3.
/app和src/不是一回事)
- [1. `RUN` 和 `CMD` 不是一回事](#1.
- 十三、这一部分学完后应该掌握什么
- 十四、这一篇最值得记住的一句话
- 十五、下一步要学什么
本专栏文章导航
- 第 01 篇:Docker 入门学习笔记 01:它到底解决了什么问题,镜像和容器又是什么
- 第 02 篇:Docker 入门学习笔记 02:基础命令、前后台运行,以及 attach、logs、exec 的区别
- 第 03 篇:Docker 入门学习笔记 03:端口映射到底是什么,为什么容器启动了却访问不到
- 第 04 篇:Docker 入门学习笔记 04:环境变量到底在做什么,为什么很多容器都依赖它
- 第 05 篇:Docker 入门学习笔记 05:卷到底是什么,为什么容器删了数据却还能保留
- 第 06 篇:Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile
- 第 07 篇:Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose
这一篇开始进入 Docker 学习的关键阶段:
从"会运行别人的镜像",进入"自己构建镜像"。
前面已经学过怎么拉镜像、启动容器、看日志、传环境变量和挂载目录;这一篇要解决的是另一个更实际的问题:
自己的项目,怎么打包成镜像,并且让别人也能完整复现?
为了让案例更贴近常见 Python 场景,这里使用一个很小的数据分析示例。这个版本做了两点收敛:
- 去掉
numpy - 代码里只显式使用
pandas
之所以还能画图,是因为 pandas 的 .plot() 底层仍然依赖 matplotlib。所以依赖里保留 matplotlib,但不再额外写 numpy 逻辑。这样镜像通常会比三包版本更轻一些。
一、先看完整目录结构
完整目录如下:
text
python-docker-demo/
src/
main.py
data/
sample.csv
output/
requirements.txt
Dockerfile
这个结构已经是一个可以真实打包的最小项目了,里面同时包含:
- 代码目录
- 数据目录
- 输出目录
- 依赖清单
- Dockerfile
二、为什么这次必须把源码和数据文件都列出来
如果文档只写 Dockerfile,不写项目文件内容,读者通常只能理解思路,不能直接复现。
而这篇的目标不是"看懂一点点",而是:
读者跟着文档把文件敲出来,就能真的 build 和 run。
所以这一篇把以下文件全部列出来:
requirements.txtsrc/main.pydata/sample.csvDockerfile
三、这个案例到底在做什么
这个小程序会完成 4 件事:
- 从
data/sample.csv读取数据 - 用
pandas做简单计算 - 用
pandas.plot()画出折线图 - 把结果写到
output/目录
最终会生成两个输出文件:
summary.txtchart.png
这个案例很适合用来理解 4 个核心问题:
- 代码怎么进入镜像
- 数据怎么进入镜像
- 依赖怎么安装到镜像里
- 为什么有时候结果只在容器里,有时候又会落到本地
四、先把项目文件全部写出来
1. requirements.txt
txt
pandas==2.2.2
matplotlib==3.8.4
2. data/sample.csv
csv
day,value
1,10
2,15
3,13
4,20
5,18
6,25
7,30
这是程序要读取的输入数据。
3. src/main.py
python
from pathlib import Path
import pandas as pd
BASE_DIR = Path("/app")
DATA_FILE = BASE_DIR / "data" / "sample.csv"
OUTPUT_DIR = BASE_DIR / "output"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
df = pd.read_csv(DATA_FILE)
df["double_value"] = df["value"] * 2
df["moving_avg"] = df["value"].rolling(window=3, min_periods=1).mean()
df["normalized"] = df["value"] / df["value"].max()
summary_file = OUTPUT_DIR / "summary.txt"
with open(summary_file, "w", encoding="utf-8") as f:
f.write("Data summary\n")
f.write(f"rows={len(df)}\n")
f.write(f"mean={df['value'].mean():.2f}\n")
f.write(f"max={df['value'].max()}\n")
f.write(f"min={df['value'].min()}\n")
ax = df.plot(
x="day",
y=["value", "moving_avg"],
marker="o",
figsize=(8, 4),
title="Value Trend",
)
ax.set_xlabel("day")
ax.set_ylabel("value")
ax.figure.tight_layout()
chart_file = OUTPUT_DIR / "chart.png"
ax.figure.savefig(chart_file)
print("Task finished successfully.")
print(f"Loaded file: {DATA_FILE}")
print(f"Summary saved to: {summary_file}")
print(f"Chart saved to: {chart_file}")
这个程序里最值得注意的点有 3 个:
- 输入文件固定读
/app/data/sample.csv - 输出文件固定写
/app/output - 容器里的工作路径和代码目录是两回事
这里的 /app 是容器内部的工作目录,不是宿主机目录名;而 src/ 才是项目中的代码目录。
4. Dockerfile
dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
COPY data ./data
COPY output ./output
CMD ["python", "src/main.py"]
到这里为止,读者已经具备完整复现所需的全部文件。
五、怎么理解这份 Dockerfile
1. FROM python:3.11-slim
表示:
从 python:3.11-slim 这个基础镜像开始构建。
这里最核心的理解不是记名字,而是理解选型原则:
- 如果项目主运行时是 Python,就优先从 Python 官方镜像开始
- 没必要先从 Ubuntu 起步,再手动安装 Python 和 pip
slim说明这个基础镜像是相对精简的版本
2. WORKDIR /app
表示:
后续命令默认在 /app 这个目录下执行。
如果目录原本不存在,Docker 通常会自动创建。
所以从这一行开始:
COPY requirements.txt .COPY src ./srcCMD ["python", "src/main.py"]
都会默认基于 /app 来理解。
3. COPY requirements.txt .
表示先把依赖清单复制进镜像。
这么写的好处是:
- 依赖文件变化通常比业务代码少
- 更容易利用构建缓存
这也是 Python 项目里最常见的写法之一。
4. RUN pip install --no-cache-dir -r requirements.txt
表示:
在构建镜像时安装依赖。
这里一定要和 CMD 区分开:
RUN发生在构建阶段CMD发生在容器启动阶段
--no-cache-dir 的作用是尽量减少 pip 缓存残留,通常有助于减小镜像体积。
5. COPY src ./src
6. COPY data ./data
7. COPY output ./output
这 3 行分别把代码、输入数据和输出目录结构复制进镜像。
注意这里的 COPY output ./output 不是说结果已经生成,而是先把这个目录结构准备好。
8. CMD ["python", "src/main.py"]
表示:
容器启动时默认执行 python src/main.py。
这就是镜像被 docker run 后真正启动的主命令。
六、如何一步步复现这个项目
先进入项目目录:
bash
cd python-docker-demo
然后构建镜像:
bash
docker build -t python-data-plot-demo:1.0 .
构建时通常能看到类似步骤:
text
[1/7] FROM docker.io/library/python:3.11-slim
[2/7] WORKDIR /app
[3/7] COPY requirements.txt .
[4/7] RUN pip install --no-cache-dir -r requirements.txt
[5/7] COPY src ./src
[6/7] COPY data ./data
[7/7] COPY output ./output
这说明镜像不是一次生成的,而是从基础镜像开始,一层一层叠加出来的。
构建完成后可以执行:
bash
docker images
这时会看到自己的镜像。具体大小会因基础镜像、缓存和平台环境略有差异。
七、第一次运行:不挂载本地目录
先执行:
bash
docker run --rm python-data-plot-demo:1.0
输出通常类似:
text
Task finished successfully.
Loaded file: /app/data/sample.csv
Summary saved to: /app/output/summary.txt
Chart saved to: /app/output/chart.png
这说明 3 件事:
- 镜像构建成功
- 容器启动时正确执行了
src/main.py - 程序已经在容器中读到了数据并生成了结果
但这时还有一个关键点:
输出文件默认写在容器内部。
而本次命令用了:
bash
--rm
这表示容器退出后会自动删除,所以如果没有额外挂载目录,结果不会留在宿主机项目目录里。
八、第二次运行:把输出目录挂载回本地
再执行:
bash
docker run --rm -v "$(pwd)/output:/app/output" python-data-plot-demo:1.0
程序输出看起来和上一次很像,但含义已经不同了。
-v "$(pwd)/output:/app/output" 表示:
- 宿主机当前目录下的
output/ - 挂载到容器里的
/app/output
这意味着程序往容器中的 /app/output 写文件时,实际上是在写宿主机的 output/ 目录。
所以运行后执行:
bash
ls output
通常就能看到:
summary.txtchart.png
九、为什么挂载后,容器里的目录会"变成"本地目录
这是 Docker 初学者最容易混淆的点之一。
Dockerfile 里有:
dockerfile
COPY output ./output
这表示镜像内部原本就有 /app/output 这个目录。
但运行时如果再加:
bash
-v "$(pwd)/output:/app/output"
那么容器实际看到的 /app/output,优先就是宿主机这个目录。
因此要记住这组区别:
COPY决定镜像里原本有什么-v决定容器运行时实际使用什么
运行时挂载一旦生效,容器访问的就是挂载内容,而不是镜像里原本那层目录内容。
十、如果把本地数据目录挂载到 /app/data
例如:
bash
docker run --rm -v "$(pwd)/data_local:/app/data" python-data-plot-demo:1.0
这时容器运行时看到的 /app/data,优先就是宿主机的 data_local/,而不是镜像里原本复制进去的 data/。
这意味着:
- 镜像里本来有
/app/data/sample.csv - 运行时挂载后,这个路径会被宿主机目录接管
- 镜像里的文件不会自动同步到宿主机目录
最值得记住的一句话是:
挂载不会自动把镜像里的文件复制到宿主机,它只是让容器路径改为使用宿主机目录。
十一、这个案例真正串起了哪些知识
这个案例已经把前面学过的很多内容串起来了:
Dockerfile决定镜像如何构建requirements.txt管理 Python 依赖COPY负责把代码和数据放进镜像RUN在构建阶段安装依赖CMD定义容器启动时默认执行的命令-v在运行阶段把输出目录映射回宿主机
所以这个案例的真正价值不只是"跑了一个 Python 程序",而是:
第一次把构建阶段和运行阶段的区别真正串起来了。
十二、这一部分最容易混淆的点
1. RUN 和 CMD 不是一回事
RUN:构建镜像时执行CMD:容器启动时默认执行
2. COPY 和 -v 不是一回事
COPY:构建时把文件放进镜像-v:运行时把宿主机目录挂进容器
3. /app 和 src/ 不是一回事
/app:容器内工作目录src/:项目中的代码目录
十三、这一部分学完后应该掌握什么
如果这一部分真正掌握了,应该能清楚说出这些内容:
- Dockerfile 是构建镜像的说明书
FROM、WORKDIR、COPY、RUN、CMD是最核心的 Dockerfile 指令- Python 项目通常适合直接基于 Python 官方镜像构建
requirements.txt是 Python 项目管理依赖的常见方式RUN发生在构建阶段,CMD发生在运行阶段COPY是把文件打进镜像,-v是运行时挂载目录- 运行时挂载会改变容器实际看到的目录内容
十四、这一篇最值得记住的一句话
如果只记一句话,最值得记住的是:
Dockerfile 决定镜像里原本有什么,运行时挂载决定容器最终实际使用什么目录。
十五、下一步要学什么
理解了 Dockerfile 之后,下一步最自然的延伸就是:
Docker Compose
因为已经会构建并运行单个容器了,接下来要进入更真实的问题:
如果一个项目里不止一个容器,应该怎么统一组织和启动?
本专栏文章导航
- 第 01 篇:Docker 入门学习笔记 01:它到底解决了什么问题,镜像和容器又是什么
- 第 02 篇:Docker 入门学习笔记 02:基础命令、前后台运行,以及 attach、logs、exec 的区别
- 第 03 篇:Docker 入门学习笔记 03:端口映射到底是什么,为什么容器启动了却访问不到
- 第 04 篇:Docker 入门学习笔记 04:环境变量到底在做什么,为什么很多容器都依赖它
- 第 05 篇:Docker 入门学习笔记 05:卷到底是什么,为什么容器删了数据却还能保留
- 第 06 篇:Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile
- 第 07 篇:Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose