Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile

Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile

文章目录

本专栏文章导航

这一篇开始进入 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.txt
  • src/main.py
  • data/sample.csv
  • Dockerfile

三、这个案例到底在做什么

这个小程序会完成 4 件事:

  1. data/sample.csv 读取数据
  2. pandas 做简单计算
  3. pandas.plot() 画出折线图
  4. 把结果写到 output/ 目录

最终会生成两个输出文件:

  • summary.txt
  • chart.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 ./src
  • CMD ["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.txt
  • chart.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. RUNCMD 不是一回事

  • RUN:构建镜像时执行
  • CMD:容器启动时默认执行

2. COPY-v 不是一回事

  • COPY:构建时把文件放进镜像
  • -v:运行时把宿主机目录挂进容器

3. /appsrc/ 不是一回事

  • /app:容器内工作目录
  • src/:项目中的代码目录

十三、这一部分学完后应该掌握什么

如果这一部分真正掌握了,应该能清楚说出这些内容:

  • Dockerfile 是构建镜像的说明书
  • FROMWORKDIRCOPYRUNCMD 是最核心的 Dockerfile 指令
  • Python 项目通常适合直接基于 Python 官方镜像构建
  • requirements.txt 是 Python 项目管理依赖的常见方式
  • RUN 发生在构建阶段,CMD 发生在运行阶段
  • COPY 是把文件打进镜像,-v 是运行时挂载目录
  • 运行时挂载会改变容器实际看到的目录内容

十四、这一篇最值得记住的一句话

如果只记一句话,最值得记住的是:

Dockerfile 决定镜像里原本有什么,运行时挂载决定容器最终实际使用什么目录。

十五、下一步要学什么

理解了 Dockerfile 之后,下一步最自然的延伸就是:

Docker Compose

因为已经会构建并运行单个容器了,接下来要进入更真实的问题:

如果一个项目里不止一个容器,应该怎么统一组织和启动?

本专栏文章导航

相关推荐
斯普信云原生组2 小时前
Docker 开源软件应急处理方案及操作手册——容器运行异常处理
docker·容器·eureka
ghie90902 小时前
基于学习的模型预测控制(LBMPC)MATLAB实现指南
开发语言·学习·matlab
Engineer邓祥浩2 小时前
JVM学习笔记(6) 第二部分 自动内存管理 第5章节 调优案例分析与实战
jvm·笔记·学习
ysa0510302 小时前
斐波那契上斐波那契【矩阵快速幂】
数据结构·c++·笔记·算法
倒酒小生2 小时前
4月7日算法学习小结
linux·服务器·学习
摆烂z2 小时前
对外访问网络限制*.aliyuncs.com开放也拉不下来和查看docker容器结构
运维·docker·容器
xinzheng新政2 小时前
Javascript·深入学习基础知识2
开发语言·javascript·学习
派大星~课堂2 小时前
【力扣-94.二叉树的中序遍历】Python笔记
笔记·python·leetcode
世人万千丶3 小时前
开源鸿蒙跨平台Flutter开发:儿童数理认知与神经塑性演化引擎_突触发生与工作记忆测绘架构
学习·flutter·华为·开源·harmonyos