原创

Dockerfile使用详解:常用指令、构建原理与最佳实践

Dockerfile 是什么,解决了什么问题

Dockerfile 是一个用于描述镜像构建过程的文本文件。它把“安装什么依赖、复制哪些文件、暴露哪些端口、容器启动时执行什么命令”这些操作固化成可重复执行的构建步骤。

在没有 Dockerfile 之前,很多团队构建运行环境的方式往往是:

  • 手工进入服务器安装依赖
  • 手工复制代码
  • 手工修改配置
  • 手工启动服务

这种方式的问题很明显:不一致、不可追溯、难以复现。 而 Dockerfile 的价值就是把环境构建过程代码化,做到:

  • 开发、测试、生产环境尽量一致
  • 镜像构建过程可审计、可版本管理
  • 新成员可以快速复现运行环境
  • CI/CD 可以自动化构建与发布

Dockerfile 的基本构建流程

当执行下面的命令时:

docker build -t myapp:1.0 .

Docker 会在当前目录中查找 Dockerfile,然后按照文件中的指令从上到下逐条执行,最终生成一个镜像。

一个典型过程是:

  1. 选择基础镜像
  2. 安装系统依赖或运行时
  3. 拷贝业务代码
  4. 设置环境变量
  5. 暴露端口
  6. 指定容器启动命令

例如:

FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 8000
CMD ["python", "app.py"]

这 6 行就描述了一个最小可运行的 Python 服务镜像。


Dockerfile 中最常用的指令详解

FROM:指定基础镜像

FROM 是 Dockerfile 的第一条有效指令,用来指定当前镜像基于哪个镜像构建。

FROM nginx:1.27

也可以指定更轻量的镜像:

FROM python:3.11-slim

使用建议

  1. 尽量使用明确版本号,不要长期依赖 latest
  2. 优先选择官方镜像
  3. 在满足需求的前提下选择更小的基础镜像,如 slimalpine

为什么不要滥用 latest

FROM node:latest

这种写法的风险在于:同一份 Dockerfile,今天构建和下个月构建,拿到的可能不是同一个基础环境,容易导致“昨天还能跑,今天突然出问题”。

更稳妥的方式:

FROM node:20.12-slim

WORKDIR:设置工作目录

WORKDIR 用于指定后续指令的执行目录。如果目录不存在,会自动创建。

WORKDIR /app

之后的 COPYRUNCMD 默认都会在这个目录上下文中执行。

例如:

WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

等价于进入 /app 目录后再执行这些命令。

为什么推荐使用 WORKDIR,而不是 RUN cd

错误示例:

RUN cd /app && pip install -r requirements.txt

原因在于每个 RUN 都是一个新的构建层,前一个 RUN 中的 cd 状态不会自动保留。 所以更推荐:

WORKDIR /app
RUN pip install -r requirements.txt

COPY:复制文件到镜像中

COPY 是最常用的文件复制指令。

COPY . /app

表示把当前构建上下文中的所有文件复制到镜像中的 /app 目录。

也可以精确复制:

COPY requirements.txt /app/
COPY src/ /app/src/

推荐做法:先复制依赖清单,再复制源码

例如 Python 项目:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

这样做的好处是:当业务代码变化,但依赖文件没有变化时,Docker 可以复用依赖安装这一层缓存,提高构建速度。


ADD:功能比 COPY 多,但不建议滥用

ADD 除了复制文件,还支持:

  • 自动解压本地压缩包
  • 从 URL 下载资源(不推荐依赖)

例如:

ADD app.tar.gz /app/

COPY 和 ADD 的区别

指令 主要用途 是否推荐
COPY 纯粹复制文件 推荐,语义清晰
ADD 复制 + 解压 + 远程下载 谨慎使用

多数场景下,优先使用 COPY。只有明确需要自动解压等行为时再考虑 ADD


RUN:构建镜像时执行命令

RUN 用于在镜像构建阶段执行命令,常见用途有:

  • 安装系统包
  • 安装语言依赖
  • 编译代码
  • 清理缓存

例如:

RUN apt-get update && apt-get install -y curl

Shell 格式与 Exec 格式

Shell 格式

RUN apt-get update && apt-get install -y curl

由 shell 解析,写法直观,适合链式命令。

Exec 格式

RUN ["apt-get", "update"]

使用 JSON 数组格式,避免 shell 解析差异,但复杂命令可读性稍弱。

多数情况下,RUN 使用 shell 格式更常见。

RUN 的优化建议

把相关命令尽量合并,减少镜像层数:

RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

这样既减少层数,又能顺手清理缓存,避免镜像体积膨胀。


CMD:指定容器默认启动命令

CMD 定义的是容器启动时默认执行的命令

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

一个 Dockerfile 中通常只有最后一个 CMD 生效。

两种写法

Exec 格式,推荐

CMD ["nginx", "-g", "daemon off;"]

Shell 格式

CMD python app.py

推荐使用 Exec 格式,因为它的信号处理更直接,也更适合生产环境。


ENTRYPOINT:定义固定入口命令

ENTRYPOINTCMD 很容易混淆。

  • ENTRYPOINT:定义容器的固定入口
  • CMD:定义默认参数,或默认命令

示例:

ENTRYPOINT ["python"]
CMD ["app.py"]

容器启动时实际执行的是:

python app.py

如果运行:

docker run myapp other.py

那么 CMD 会被替换,最终执行:

python other.py

什么时候用 ENTRYPOINT

当你希望镜像始终表现得像一个固定工具时,适合使用 ENTRYPOINT。 例如某镜像的本质就是执行 pythonjavanginx,这时 ENTRYPOINT 很自然。

CMD 和 ENTRYPOINT 的关系

最常见组合是:

ENTRYPOINT ["gunicorn"]
CMD ["app:app", "-b", "0.0.0.0:8000"]

这样用户既能直接运行服务,也能在 docker run 时覆盖参数。


ENV:设置环境变量

ENV 用于在镜像中设置环境变量。

ENV APP_ENV=production
ENV PORT=8000

容器运行时可以读取这些变量。

例如 Python:

import os
port = os.getenv("PORT", "8000")

ENV 与 ARG 的区别

  • ENV:镜像中长期存在,运行时也可见
  • ARG:只在构建阶段可用,镜像运行时不可见

ARG:构建时参数

ARG 用于在 docker build 时传参。

ARG APP_VERSION=1.0.0
RUN echo "building version: ${APP_VERSION}"

构建时可以覆盖:

docker build --build-arg APP_VERSION=1.2.3 -t myapp:1.2.3 .

典型用途

  • 传入版本号
  • 选择构建环境
  • 控制依赖源或编译参数

但要注意:ARG 不适合存储敏感信息,因为它可能出现在镜像历史中。


EXPOSE:声明容器监听端口

EXPOSE 8080

EXPOSE 的作用是文档化告诉使用者:这个容器预期监听 8080 端口。 它不会自动把宿主机端口映射出来。

真正对外发布端口仍然需要:

docker run -p 8080:8080 myapp

USER:指定容器运行用户

默认情况下,容器通常以 root 用户运行。生产环境中这并不安全。

RUN useradd -m appuser
USER appuser

这样容器中的应用将以普通用户身份运行,能降低误操作和安全风险。

为什么生产环境推荐非 root

  • 减少容器被入侵后的权限范围
  • 避免应用直接修改敏感系统文件
  • 更符合最小权限原则

VOLUME:声明挂载点

VOLUME ["/data"]

表示 /data 是一个可挂载的数据卷目录,适合存储需要持久化的数据。

不过在现代项目中,很多团队更习惯在运行时通过 docker run -vdocker-compose.yml / compose.yaml 显式管理卷,而不是完全依赖 Dockerfile 中的 VOLUME


HEALTHCHECK:定义健康检查

健康检查用于让 Docker 判断容器是否真正可用,而不只是“进程还活着”。

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

这在 Web 服务中非常有用。因为很多服务虽然进程还在,但实际上已经无法对外提供请求。


Dockerfile 的分层机制与缓存原理

Dockerfile 每执行一条会生成文件系统变更的指令,通常就会形成一个新的镜像层。 这也是为什么 Docker 构建速度有时很快,有时很慢:缓存是否命中非常关键。

例如:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

当你只改了 app.py,重新构建时:

  • FROM 命中缓存
  • WORKDIR 命中缓存
  • COPY requirements.txt 命中缓存
  • RUN pip install -r requirements.txt 命中缓存
  • COPY . . 这层失效
  • CMD 重新应用

这就是为什么依赖文件应该尽早单独复制,而业务代码后复制。

缓存优化的核心原则

  1. 变化少的步骤写前面
  2. 变化频繁的步骤写后面
  3. 大量依赖安装尽量依赖固定文件
  4. 构建上下文尽量小,避免无关文件导致缓存失效

.dockerignore 为什么很重要

很多人写 Dockerfile 时只关注指令本身,却忽略了 .dockerignore。 这个文件的作用类似 .gitignore,用于告诉 Docker 构建时不要把哪些文件发送到构建上下文

示例:

.git
node_modules
__pycache__
*.log
dist
build
.env

不写 .dockerignore 的后果

  1. 构建上下文变大,上传到 Docker daemon 更慢
  2. 无关文件进入镜像,镜像体积膨胀
  3. 敏感文件误复制进镜像
  4. 无意义文件变化导致缓存频繁失效

.dockerignore 是 Dockerfile 优化里非常容易被低估的一环。


一个完整示例:Python Web 服务的 Dockerfile

下面是一个更接近实际项目的例子:

FROM python:3.11-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl gcc && \
    rm -rf /var/lib/apt/lists/*

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

COPY . .

RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

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

这份 Dockerfile 做了什么

  • 基于轻量级 Python 镜像
  • 设置工作目录为 /app
  • 配置 Python 运行环境变量
  • 安装必要系统依赖
  • 先安装 Python 依赖以利用缓存
  • 复制业务代码
  • 切换到非 root 用户
  • 暴露 8000 端口
  • 增加健康检查
  • 指定服务启动命令

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

很多项目在构建阶段需要安装编译工具,但运行阶段并不需要。 如果把这些工具全部留在最终镜像中,体积会很大。

这时就可以使用多阶段构建

示例:

FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app main.go

FROM debian:stable-slim
WORKDIR /app
COPY --from=builder /src/app /app/app
EXPOSE 8080
CMD ["/app/app"]

多阶段构建的优点

  1. 最终镜像更小
  2. 运行环境更干净
  3. 减少攻击面
  4. 提高分发效率

适用场景

  • Go、Rust、C/C++ 编译型项目
  • 前端项目先打包静态资源,再放入 Nginx
  • Java 项目先构建 JAR,再放入运行时镜像
  • Python 项目中需要复杂构建依赖时也可分阶段处理

Dockerfile 编写最佳实践

1. 基础镜像尽量精简且明确版本

不推荐:

FROM ubuntu

更推荐:

FROM python:3.11-slim

2. 优先使用 COPY,而不是 ADD

大多数时候你只需要复制文件,不需要额外副作用。


3. 利用缓存优化构建速度

把依赖安装与源码复制分开:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

4. 合并 RUN 指令并及时清理缓存

例如安装 apt 包时:

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

5. 不要把敏感信息写进镜像

错误示例:

ENV DB_PASSWORD=123456

这类信息不应该固化在镜像里,而应在运行时通过环境变量、密钥管理系统或编排平台注入。


6. 尽量使用非 root 用户运行应用

这是生产环境里很重要的一条基线。


7. 为服务镜像添加健康检查

尤其是 Web 服务、API 服务、消息消费服务,健康检查能帮助平台更准确判断实例状态。


8. 配合 .dockerignore 控制构建上下文

不要把无关目录一起送去构建。


9. 一个镜像只做一件主要事情

一个容器里既跑数据库、又跑 Nginx、又跑业务服务,通常不是好设计。 更合理的方式是一个镜像承载一个主要职责,再通过编排工具协同。


10. 固定依赖版本,增强可复现性

例如:

  • Python 的 requirements.txt
  • Node.js 的 package-lock.json
  • Java 的明确 JDK/JRE 版本
  • 系统包的明确来源与版本策略

这样更利于构建结果稳定。


常见问题与易踩坑

1. CMD 写了多个,为什么只有最后一个生效

因为 Dockerfile 中同类指令通常以后者覆盖前者,CMD 只保留最后一个。


2. EXPOSE 了端口,为什么还是访问不到

因为 EXPOSE 只是声明端口,不会自动映射到宿主机。还需要:

docker run -p 8000:8000 myapp

3. RUN cd /app 后,下一行为什么不在 /app 目录

因为每个 RUN 都在独立层中执行,目录状态不会自然延续。 应该使用 WORKDIR


4. 为什么镜像体积特别大

常见原因有:

  • 基础镜像太大
  • 安装了大量不必要工具
  • 没有清理包管理器缓存
  • node_modules、构建产物、日志等都复制进去了
  • 没有使用多阶段构建

5. 为什么每次改一行代码都要重新安装全部依赖

通常是因为 Dockerfile 顺序不合理,例如直接先 COPY . .,导致依赖安装层缓存失效。

错误示例:

COPY . .
RUN pip install -r requirements.txt

更合理:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

6. Shell 格式和 Exec 格式该怎么选

经验上可以这样理解:

  • RUN 常用 shell 格式,便于写链式命令
  • CMDENTRYPOINT 优先使用 exec 格式,利于信号处理和参数传递

如何读懂一个 Dockerfile

当你拿到一份陌生 Dockerfile 时,可以按下面顺序快速分析:

  1. FROM:确定运行时基础环境
  2. WORKDIR:确认项目目录
  3. COPY / ADD:确认哪些文件进入镜像
  4. RUN:确认安装了哪些依赖、做了哪些构建
  5. ENV / ARG:确认配置项来源
  6. EXPOSE:确认服务端口
  7. USER:确认运行身份
  8. CMD / ENTRYPOINT:确认容器启动方式

这套阅读顺序能帮助你快速判断镜像的职责、运行方式和优化空间。


Dockerfile 与镜像、容器的关系

很多初学者容易混淆这三个概念:

  • Dockerfile:构建说明书
  • 镜像 Image:按照说明书构建出来的静态产物
  • 容器 Container:镜像启动后的运行实例

可以简单理解为:

  • Dockerfile 像“菜谱”
  • 镜像像“半成品套餐”
  • 容器像“真正端上桌并开始食用的菜”

理解这个关系后,再看 docker builddocker run 的区别就很清晰了:

  • docker build:根据 Dockerfile 生成镜像
  • docker run:根据镜像启动容器

总结

Dockerfile 的本质不是“写几条命令把程序跑起来”,而是把运行环境标准化、可复现化、自动化

真正写好 Dockerfile,关键不在于记住多少条指令,而在于理解三个核心点:

  1. 镜像是分层构建的,顺序会影响缓存与体积
  2. 容器运行环境应该尽量最小化,减少体积和风险
  3. 构建过程要可重复、可维护、可审计

在实际项目中,建议至少做到以下基线:

  • 固定基础镜像版本
  • 合理利用缓存
  • 使用 .dockerignore
  • 尽量非 root 运行
  • 避免把敏感信息写进镜像
  • 优先采用多阶段构建优化体积

只要掌握这些原则,你就不仅能“写出能用的 Dockerfile”,还能写出适合生产环境的 Dockerfile

正文到此结束
评论插件初始化中...
Loading...