05. Dockerfile 编写实战:把自己的 Python/Node.js 应用打包成镜像
前面几篇文章我们都在用别人做好的镜像,docker pull nginx、docker pull python……但每次都得手动进容器改配置、装依赖、拷代码,太费劲了。能不能把我写好的应用直接打包成一个镜像,以后 docker run 一把梭就跑起来?今天就来搞定这件事——学会写 Dockerfile,让你的应用也能变成 Docker 镜像。

▲ 别把 Dockerfile 当复杂语法,它就像一份"菜谱":基础镜像、工作目录、依赖、代码、启动命令,照着一步步来就能"出锅"。
一、Dockerfile 是什么?一份"菜谱"
你可以把 Dockerfile 想象成一份菜谱:
-
你告诉 Docker:"先准备什么食材(基础镜像)、用什么锅(工作目录)、放什么调料(依赖)、加什么主菜(应用代码)、最后怎么上菜(启动命令)"
-
Docker 照着这份菜谱一步步执行,最后给你做出一道完整的"菜"——也就是一个镜像
技术上说,Dockerfile 是一个纯文本文件(就叫 Dockerfile,没有扩展名),里面写满了 Docker 能识别的指令。你用 docker build 命令告诉 Docker "照着这个文件构建镜像",它就会逐行执行这些指令。
一句话:Dockerfile 是镜像的源代码,镜像是 Dockerfile 编译后的产物。
二、常用指令速查表
先混个脸熟,后面会一个个详细讲:
| FROM | 指定基础镜像 | 选个锅 |
| WORKDIR | 设置工作目录 | 进厨房 |
| COPY | 复制文件到镜像 | 放食材 |
| ADD | 复制文件(支持自动解压 tar) | 放食材+开罐头 |
| RUN | 执行命令(装软件、编译等) | 烹饪过程 |
| ENV | 设置环境变量 | 调口味 |
| EXPOSE | 声明端口号(仅文档说明) | 贴标签 |
| CMD | 容器启动时执行的默认命令 | 上菜方式 |
| ENTRYPOINT | 容器启动时执行的主命令 | 固定菜品 |
| ARG | 构建时变量(运行时不可用) | 厨师备注 |
三、常用指令详解(每条都有示例)
3.1 FROM:指定基础镜像
每条 Dockerfile 的第一行必须是 FROM。 它决定了你的镜像"站在谁的肩膀上":
# Python 应用常用
FROM python:3.11-slim
# Node.js 应用常用
FROM node:20-alpine
# 极简场景(只跑个二进制文件)
FROM alpine:3.19
# Java 应用
FROM eclipse-temurin:17-jdk-alpine
选镜像版本的技巧:
| latest | 较大 | 不推荐生产使用(版本不固定) |
| 3.11 | 中等 | 开发调试够用 |
| 3.11-slim | 小(约 120MB) | 推荐生产环境 |
| 3.11-alpine | 极小(约 50MB) | 追求极致体积(但某些库编译会报错) |
建议:生产环境用 -slim 版本,兼顾体积和兼容性。
3.2 WORKDIR:设置工作目录
相当于在容器里 cd 到某个目录,后续所有指令都在这个目录下执行:
# 创建并进入 /app 目录(目录不存在会自动创建)
WORKDIR /app
# 后面的 COPY、RUN 都在 /app 下操作
COPY . .
RUN pip install -r requirements.txt
好习惯:永远不要省略 WORKDIR。 不设的话,默认在 / 根目录操作,文件散落在根目录既乱又不安全。
3.3 COPY vs ADD:复制文件进镜像
这两个都能把宿主机文件复制到镜像里,但有区别:
# COPY:简单粗暴,只干复制这一件事
COPY requirements.txt /app/
COPY . /app/
# ADD:多了两个"超能力"
ADD archive.tar.gz /app/ # 自动解压 tar 包
ADD https://example.com/file.zip /tmp/ # 可以从 URL 下载
对比表:
| 复制本地文件 | ✅ | ✅ |
| 自动解压 tar | ❌ | ✅ |
| 支持 URL 下载 | ❌ | ✅ |
| 推荐使用 | 推荐 | 只在需要解压时用 |
最佳实践:99% 的场景用 COPY 就够了。 ADD 的"超能力"容易引发意外行为(比如你以为只是复制,结果它给你解压了),所以 Docker 官方也建议优先用 COPY。
3.4 RUN:执行命令
RUN 用来在构建镜像时执行命令,最常见的用途是安装依赖:
# Ubuntu/Debian 系装软件
RUN apt update && apt install -y curl git
# Python 装依赖
RUN pip install flask requests
# Node.js 装依赖
RUN npm install
⚠️ 层缓存陷阱(新手必知)
每一条 RUN 都会在镜像里创建一个新层。反模式:
# ❌ 错误示范:三条 RUN 产生三个层,而且 apt 缓存占空间
RUN apt update
RUN apt install -y curl
RUN apt install -y git
正确做法:合并命令 + 清理缓存
# ✅ 正确示范:一条 RUN,清理缓存减小体积
RUN apt update \\
&& apt install -y –no-install-recommends curl git \\
&& rm -rf /var/lib/apt/lists/*
3.5 ENV:设置环境变量
# 格式 1:单个变量
ENV APP_ENV=production
# 格式 2:多个变量
ENV APP_PORT=8080 APP_HOST=0.0.0.0
# 在后续指令和容器运行时都能用到
RUN echo "当前环境:$APP_ENV"
CMD python app.py –port $APP_PORT
3.6 EXPOSE:声明端口(但别当真)
# 声明应用监听的端口
EXPOSE 8080
EXPOSE 5000
注意:EXPOSE 只是个"文档说明",不会真的映射端口!
要让外部访问,必须在 docker run 时用 -p 参数:
# EXPOSE 8080 只是告诉你"这个应用用 8080 端口"
# 真正映射端口还得靠 -p
docker run -p 8080:8080 myapp
把它想象成在包装盒上贴标签:"本产品使用 8080 端口"——贴不贴标签,产品本身都一样工作。
3.7 CMD vs ENTRYPOINT:最容易搞混的两个指令
这是 Dockerfile 里最容易踩坑的地方,务必仔细看清楚。
核心区别
| 作用 | 容器启动时的默认命令 | 容器启动时的主命令 |
| 被覆盖 | docker run 后加参数会被覆盖 | 不会被覆盖(参数会追加) |
| 适用场景 | 可灵活替换的默认行为 | 固定的主程序入口 |
代码对比
# 使用 CMD
FROM python:3.11-slim
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
运行:
# 正常启动:执行 python app.py
docker run myapp
# 覆盖 CMD:执行 bash(进入了 shell,不启动应用了)
docker run -it myapp bash
# 使用 ENTRYPOINT
FROM python:3.11-slim
WORKDIR /app
COPY . .
ENTRYPOINT ["python", "app.py"]
运行:
# 正常启动:执行 python app.py
docker run myapp
# 追加参数:执行 python app.py –debug
docker run myapp –debug
# 想覆盖 ENTRYPOINT:必须显式指定 –entrypoint
docker run –entrypoint bash -it myapp
黄金组合
最佳实践:ENTRYPOINT + CMD 搭配使用
# ENTRYPOINT 是"主菜"(固定的)
# CMD 是"配菜"(可替换的默认参数)
ENTRYPOINT ["python"]
CMD ["app.py"]
这样:
-
docker run myapp → 执行 python app.py
-
docker run myapp script.py → 执行 python script.py(覆盖了默认参数)
3.8 .dockerignore:排除不需要的文件
在项目根目录创建一个 .dockerignore 文件,类似 .gitignore:
# Python 项目
__pycache__/
*.pyc
*.pyo
.git/
.gitignore
venv/
.venv/
*.md
.env
.pytest_cache/
# Node.js 项目
node_modules/
npm-debug.log*
.DS_Store
为什么需要它?
如果你 COPY . . 把整个项目拷进镜像,会把这些垃圾也拷进去:
-
node_modules/:几百 MB 的依赖,容器里会重装
-
.git/:整个 Git 历史,白白占空间
-
venv/:本地虚拟环境,容器里用不到
不加 .dockerignore,镜像体积可能膨胀好几倍,构建速度也会变慢。
四、实战 1:Python Flask 应用完整打包
来一个真实场景:把一个 Flask Web 应用打包成 Docker 镜像。
4.1 项目结构
my-flask-app/
├── app.py # Flask 应用主程序
├── requirements.txt # Python 依赖
├── Dockerfile # Docker 菜谱
└── .dockerignore # 排除文件
4.2 编写 Flask 应用
# app.py
from flask import Flask, jsonify
import socket
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify({
"message": "Hello from Docker!",
"hostname": socket.gethostname(),
"python_version": "3.11"
})
@app.route('/health')
def health():
return jsonify({"status": "healthy"})
if __name__ == '__main__':
# 注意:host 必须是 0.0.0.0,否则容器外访问不到
app.run(host='0.0.0.0', port=5000)
# requirements.txt
flask==3.0.0
4.3 编写 Dockerfile
# 1. 选择基础镜像
FROM python:3.11-slim
# 2. 设置工作目录
WORKDIR /app
# 3. 先复制依赖文件(利用层缓存,后面会讲)
COPY requirements.txt .
# 4. 安装依赖
RUN pip install –no-cache-dir -r requirements.txt
# 5. 再复制应用代码
COPY . .
# 6. 声明端口(文档说明)
EXPOSE 5000
# 7. 设置环境变量
ENV FLASK_APP=app.py
# 8. 启动命令
CMD ["python", "app.py"]
4.4 构建镜像
# 在项目目录下执行(注意最后那个点 .)
docker build -t my-flask-app:v1 .
构建输出:
[+] Building 15.3s (9/9)
=> [1/5] FROM docker.io/library/python:3.11-slim 3.2s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY requirements.txt . 0.0s
=> [4/5] RUN pip install –no-cache-dir -r requirements 8.5s
=> [5/5] COPY . . 0.1s
=> exporting to image 3.4s
=> => naming to docker.io/library/my-flask-app:v1
4.5 运行容器
docker run -d -p 5000:5000 –name flask-demo my-flask-app:v1
测试访问:
curl http://localhost:5000/
输出:
{
"hostname": "a3f5b7c8d9e1",
"message": "Hello from Docker!",
"python_version": "3.11"
}
看到 hostname 是容器 ID 而不是你的本机名,说明应用确实在容器里跑起来了!
五、实战 2:Node.js Express 应用完整打包
5.1 项目结构
my-express-app/
├── index.js # Express 应用主程序
├── package.json # Node.js 依赖声明
├── Dockerfile # Docker 菜谱
└── .dockerignore # 排除文件
5.2 编写 Express 应用
// index.js
const express = require('express');
const os = require('os');
const app = express();
app.get('/', (req, res) => {
res.json({
message: 'Hello from Docker!',
hostname: os.hostname(),
platform: os.platform(),
node_version: process.version
});
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', uptime: process.uptime() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
// package.json
{
"name": "my-express-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
5.3 编写 Dockerfile
# 1. 选择基础镜像(alpine 版本极小,约 180MB)
FROM node:20-alpine
# 2. 设置工作目录
WORKDIR /app
# 3. 先复制 package.json(利用层缓存)
COPY package*.json ./
# 4. 安装生产依赖(–production 不装 devDependencies)
RUN npm install –production
# 5. 再复制应用代码
COPY . .
# 6. 声明端口
EXPOSE 3000
# 7. 启动命令
CMD ["node", "index.js"]
5.4 构建和运行
# 构建
docker build -t my-express-app:v1 .
# 运行
docker run -d -p 3000:3000 –name express-demo my-express-app:v1
# 测试
curl http://localhost:3000/
输出:
{
"message": "Hello from Docker!",
"hostname": "b4e6c8d0f2a3",
"platform": "linux",
"node_version": "v20.11.0"
}
六、构建上下文:那个点 . 到底是什么意思?
docker build -t myapp:v1 .
# ↑
# 这个点是啥?
这个点叫构建上下文(Build Context),它告诉 Docker:"把当前目录下的所有文件发送给 Docker 守护进程,供构建使用。"
为什么要发送文件?
因为 Dockerfile 里的 COPY . . 需要知道"从哪里拷"——答案就是构建上下文。
常见错误:
# ❌ 错误:上下文是 /home/user,但 Dockerfile 在子目录里
cd /home/user
docker build -t myapp myproject/Dockerfile
# 报错:unable to prepare context
# ✅ 正确:上下文是包含 Dockerfile 的目录
cd /home/user/myproject
docker build -t myapp .
也可以指定其他路径:
# 上下文是 /path/to/project,Dockerfile 也在那
docker build -t myapp /path/to/project
# 上下文是当前目录,Dockerfile 用别的名字
docker build -t myapp -f Dockerfile.prod .
记住:构建上下文决定了哪些文件能被 COPY 进镜像,也决定了 .dockerignore 要放在哪里。
七、镜像层缓存优化:顺序真的很重要
Docker 构建镜像时,每一行指令都会生成一层。如果某一层的内容没变,Docker 会直接用缓存,不再重新执行。

▲ 层缓存的精髓:把依赖清单放前面、应用代码放后面,只有改依赖才会触发"重装依赖"。
7.1 错误顺序:每次都重装依赖
# ❌ 每次改代码都要重装依赖(超慢)
FROM python:3.11-slim
WORKDIR /app
COPY . . # 拷入全部代码(包括 app.py)
RUN pip install -r requirements.txt # 只要代码变了,这一层就重跑
CMD ["python", "app.py"]
问题:你改了 app.py 一行代码 → COPY . . 这一层失效 → 后面的 RUN pip install 也跟着失效 → 每次构建都重装依赖,慢得要死。
7.2 正确顺序:先拷依赖文件,再拷代码
# ✅ 改代码不会重装依赖(超快)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . # 只拷入依赖文件
RUN pip install -r requirements.txt # 依赖没变就用缓存
COPY . . # 再拷入应用代码(改代码只影响这一层)
CMD ["python", "app.py"]
对比效果:
| 首次构建 | ~30s | ~30s |
| 只改了 app.py | ~30s(重装依赖) | ~2s(用缓存) |
| 改了 requirements.txt | ~30s | ~30s(必须重装) |
一句话总结:把不常变的文件(依赖清单)放前面,常变的文件(应用代码)放后面。
八、本文要点回顾
Dockerfile 是一份"菜谱",告诉 Docker 怎么一步步构建镜像;
FROM 必须第一行,选择合适的基础镜像(推荐 -slim 版本);
COPY vs ADD:99% 场景用 COPY,ADD 只在需要自动解压时用;
CMD vs ENTRYPOINT:CMD 可被覆盖,ENTRYPOINT 固定执行,推荐两者搭配使用;
EXPOSE 只是文档说明,真正映射端口靠 docker run -p;
.dockerignore 必须写,排除 node_modules、.git、venv 等大文件;
构建上下文:docker build 最后那个 . 决定了哪些文件能被拷进镜像;
层缓存优化:先 COPY requirements.txt 再 COPY . .,改代码不重装依赖。
下集预告
镜像打包好了,但生产环境不会只用一个容器。下一章我们学习 Docker 网络——多个容器之间怎么互相通信?怎么让 Flask 容器访问 MySQL 容器?Bridge 网络、Host 网络、自定义网络各自怎么用?敬请期待!
附:完整 Dockerfile 模板(可直接复制使用)
Python 应用模板
FROM python:3.11-slim
WORKDIR /app
# 先拷依赖
COPY requirements.txt .
RUN pip install –no-cache-dir -r requirements.txt
# 再拷代码
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
Node.js 应用模板
FROM node:20-alpine
WORKDIR /app
# 先拷依赖
COPY package*.json ./
RUN npm install –production
# 再拷代码
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
.dockerignore 通用模板
# 通用排除
.git/
.gitignore
.vscode/
.idea/
*.md
.env
.env.*
# Python 专属
__pycache__/
*.pyc
venv/
.venv/
.pytest_cache/
# Node.js 专属
node_modules/
npm-debug.log*
dist/
网硕互联帮助中心



评论前必须登录!
注册