这篇文章把 Dockerfile 拆开来讲,从零写出一个能跑的真实镜像。


先说 Dockerfile 是什么

Dockerfile 就是一个配置文件,用来说明"这个镜像要怎么组装"。它告诉 Docker:用什么基础系统、安装什么依赖、复制什么文件、启动时执行什么命令。

写好 Dockerfile,执行 docker build,就得到一个自定义镜像。这个镜像可以在任何装好 Docker 的机器上一键跑起来,环境完全一致。

说起来简单,但里面有些坑,新手容易踩进去。看完这篇,至少能避开大部分。


最小示例:一个 Python 镜像

先看个最简单的例子,感受一下结构:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "main.py"]

这就是一个完整可用的 Dockerfile。每一行都是一条指令,Docker 按顺序执行。

接下来逐条拆解。


核心指令

FROM:起点,决定基础镜像

FROM python:3.11-slim

FROM 是第一条指令,也是唯一必须有的。它指定用什么镜像作为起点。

写法是 镜像名:标签python:3.11-slim 是 Python 官方提供的镜像,slim 表示精简版,体积比完整版小很多。

常见的基础镜像:

镜像适用场景
python:3.11-slimPython 应用
node:20-alpineNode.js 应用
openjdk:17-slimJava 应用
nginx:alpine静态网站、Web 服务
ubuntu:22.04通用 Linux 环境
alpine:3.19追求极小体积的基础镜像

关于 Alpine:Alpine 是一个超轻量的 Linux 发行版,用它做基础镜像可以让最终镜像体积小很多。但有些软件在 Alpine 上需要额外配置兼容层,新手建议先用 Debian/Ubuntu 系。

RUN:执行命令,安装东西

RUN pip install --no-cache-dir -r requirements.txt

RUN 在镜像构建过程中执行命令。可以安装系统包、下载文件、创建目录等。

这里 pip install 装了 Python 依赖。--no-cache-dir 是不缓存下载文件,能让镜像小一点——这个习惯挺好。

多条 RUN 指令会分层执行,每条 RUN 都会生成一层。所以能用一条 RUN 搞定的事就别拆成多条,减少镜像体积:

# 分开写(不好)
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get clean

# 合并写(好)
RUN apt-get update && apt-get install -y nginx && rm -rf /var/lib/apt/lists/*

COPY:把文件复制进去

COPY requirements.txt .
COPY . .

COPY 把宿主机的文件复制到镜像里。

第一个 COPY requirements.txt . 把 requirements.txt 复制到当前工作目录。第二个 COPY . . 把当前目录的所有内容都复制进去(. 在 Docker 语境里表示 WORKDIR)。

两个 COPY 的顺序是有讲究的。把不常变化的文件(如依赖文件)放在前面,频繁变化的文件(如源代码)放在后面。这样可以利用 Docker 的缓存层:依赖没变的情况下,pip install 那一步会被复用,不会每次重新安装。

WORKDIR:设置工作目录

WORKDIR /app

WORKDIR 切换当前工作目录,后续的 COPY、RUN、CMD 等指令都在这个目录下执行。

路径不存在会自动创建。建议每个 Dockerfile 都显式设置 WORKDIR,别用绝对路径裸跑。

CMD:容器启动时干什么

CMD ["python", "main.py"]

CMD 指定容器启动时执行的命令。注意写法是 JSON 数组,不是字符串。

一个 Dockerfile 只能有一条 CMD,写多条的话只有最后一条生效。

这里启动 Python 脚本。如果你的应用是 Web 服务,可以写成:

CMD ["python", "-m", "http.server", "8000"]

ENV:设置环境变量

ENV NODE_ENV=production
ENV PORT=8080

ENV 设置环境变量,容器运行期间一直生效。

建议在 Dockerfile 里把环境的默认值设好,不用依赖启动时的 -e 参数传入。

EXPOSE:声明端口

EXPOSE 8080

EXPOSE 只是文档作用,告诉使用者这个镜像会监听哪个端口。实际映射还是要靠 docker run -p 参数。它不实际打开端口,只是元数据。

ARG:构建参数

ARG APP_VERSION=1.0.0

ARG 定义构建时变量,用户可以在 docker build 时传入:

docker build --build-arg APP_VERSION=2.0.0 -t myapp .

和 ENV 不同,ARG 只在构建阶段有效,运行时的容器看不到。


实战:写一个完整的 Dockerfile

做个实际的东西感受一下。假设你的项目结构是这样的:

myapp/
├── Dockerfile
├── requirements.txt
├── main.py
└── static/
    └── style.css

main.py 是个简单的 Flask Web 应用:

from flask import Flask, send_from_directory
import os

app = Flask(__name__)

@app.route('/')
def index():
    return send_from_directory('static', 'style.css')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

对应的 Dockerfile:

FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 先复制依赖文件,安装完再复制代码(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制所有代码
COPY . .

# 设置环境变量
ENV PORT=5000
ENV FLASK_APP=main.py

# 暴露端口
EXPOSE 5000

# 启动命令
CMD ["python", "main.py"]

构建镜像:

docker build -t my-flask-app .

运行容器:

docker run -d -p 5000:5000 --name flask-demo my-flask-app

访问 http://localhost:5000 看看效果。


多阶段构建:给镜像瘦身的技巧

写到这里,有个问题:最终镜像里包含了 Python 编译环境、pip 缓存这些运行时不需要的东西。Python 官方镜像 slim 版已经有 150MB+,加上依赖轻轻松松上 300MB。

用多阶段构建可以解决:

# 第一阶段:构建
FROM python:3.11-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

# 第二阶段:运行时
FROM python:3.11-slim

WORKDIR /app

COPY --from=builder /install /usr/local
COPY . .

ENV PORT=5000
EXPOSE 5000

CMD ["python", "main.py"]

第一阶段装了依赖,第二阶段只复制了依赖目录和代码。最终镜像只有 180MB 左右,比单阶段小很多。

关于多阶段构建更详细的用法,后面会有专门的文章来讲。


几个新手容易踩的坑

坑一:COPY 路径写错

./ 是两个东西。. 是相对于 WORKDIR 的当前目录,/ 是根目录。新手经常把 COPY . . 写成 COPY . /,结果文件全跑根目录去了。

坑二:CMD 和 ENTRYPOINT 混淆

简单理解:CMD 是给 docker run 用的默认参数,ENTRYPOINT 是固定命令,不容易被覆盖。

如果你的容器主要当命令用(比如一个 CLI 工具),用 ENTRYPOINT;如果主要当服务用,用 CMD。

# CMD:覆盖容易
CMD ["python", "app.py"]

# ENTRYPOINT:更像可执行文件
ENTRYPOINT ["python", "app.py"]

坑三:忽视 .dockerignore

.gitignore 类似,.dockerignore 可以在 COPY 时排除不需要的文件:

.git
node_modules
__pycache__
*.pyc
.env
*.log

不排除的话,COPY . . 会把整个目录打包进去,包括 .gitnode_modules 这些没用的东西,不仅拖慢构建,还可能把敏感信息打包进镜像。

坑四:RUN 命令不清理

apt-get install 之后不清理缓存、pip install 不加 --no-cache-dir,会让镜像膨胀很多。上文提到过,养成好习惯。


构建进阶:给镜像加标签和版本

# 打标签
docker build -t myapp:latest -t myapp:1.0.0 .

# 带构建参数
docker build --build-arg VERSION=1.0.0 -t myapp:1.0.0 .

latest 只是个约定,没有实际含义。建议用语义化版本号管理。


最后

Dockerfile 本身语法不多,多用几次就熟练了。关键是理解每条指令在构建阶段还是运行阶段生效,知道缓存是怎么工作的,知道镜像体积可以怎么优化。

关注不迷路 👆

更多推荐