云计算百科
云计算领域专业知识百科平台

05. Dockerfile 编写实战:把自己的 Python/Node.js 应用打包成镜像

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 下载

对比表:

特性COPYADD
复制本地文件
自动解压 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 里最容易踩坑的地方,务必仔细看清楚。

核心区别
特性CMDENTRYPOINT
作用 容器启动时的默认命令 容器启动时的主命令
被覆盖 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/

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 05. Dockerfile 编写实战:把自己的 Python/Node.js 应用打包成镜像
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!