Dockerfile 是一个文本文件,用于定义如何构建 Docker 镜像。它包含了一系列指令,告诉 Docker 如何组装镜像(例如基于什么基础镜像、复制哪些文件、安装哪些软件、设置什么环境变量等)。通过 docker build 命令,Docker 会逐条执行这些指令,最终生成一个可运行的镜像。

一、作用

  • 自动化构建:无需手动操作容器,只需编写 Dockerfile,即可重复、一致地构建镜像。
  • 环境标准化:确保开发、测试、生产环境一致,避免“在我电脑上能跑”的问题。
  • 版本控制:Dockerfile 可以像代码一样纳入 Git 管理,方便追踪和协作。
  • 简化部署:基于 Dockerfile 构建的镜像可以快速分发和运行,简化应用部署流程。

二、指令

指令 说明
FROM 指定基础镜像,用于后续的指令构建。
MAINTAINER 指定Dockerfile的作者/维护者。(已弃用,推荐使用LABEL指令)
LABEL 添加镜像的元数据,使用键值对的形式。
RUN 在构建过程中在镜像中执行命令。
CMD 指定容器创建时的默认命令。(可以被覆盖)
ENTRYPOINT 设置容器创建时的主要命令。(不可被覆盖)
EXPOSE 声明容器运行时监听的特定网络端口。
ENV 在容器内部设置环境变量。
ADD 将文件、目录或远程URL复制到镜像中。
COPY 将文件或目录复制到镜像中。
VOLUME 为容器创建挂载点或声明卷。
WORKDIR 设置后续指令的工作目录。
USER 指定后续指令的用户上下文。
ARG 定义在构建过程中传递给构建器的变量,可使用 “docker build” 命令设置。
ONBUILD 当该镜像被用作另一个构建过程的基础时,添加触发器。
STOPSIGNAL 设置发送给容器以退出的系统调用信号。
HEALTHCHECK 定义周期性检查容器健康状态的命令。
SHELL 覆盖Docker中默认的shell,用于RUN、CMD和ENTRYPOINT指令。

三、指令使用

3.1、FROM

指定基础镜像,必须是 Dockerfile 的第一条有效指令

FROM ubuntu:22.04

3.2、MAINTAINER (已弃用)

指定作者信息(已弃用,推荐用 LABEL 替代)

MAINTAINER zhangsan <zhangsan@example.com>

3.3、LABEL

添加镜像元数据,键值对形式。

LABEL version="1.0" description="这是一个Web应用镜像" maintainer="lisi"

3.4、RUN

在构建镜像时执行命令,常用来安装软件包。

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

3.5、CMD

设置容器启动时的默认命令,可被 docker run 后的命令覆盖。

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

3.6、ENTRYPOINT

设置容器启动时的主命令,不易被覆盖(除非使用 --entrypoint 参数)

ENTRYPOINT ["top", "-b"]

3.7、EXPOSE

声明容器运行时监听的端口,仅作说明,不实际发布端口。

EXPOSE 80 443

3.8、ENV

设置环境变量,供后续指令和容器内程序使用。

ENV APP_HOME /usr/src/app
ENV NODE_ENV=production

3.9、ADD

将文件、目录或远程 URL 复制到镜像中,并支持自动解压 tar 包

ADD ./app.tar.gz /usr/src/app/
ADD https://example.com/file.txt /tmp/

3.10、COPY

将文件或目录复制到镜像中(比 ADD 更纯粹,推荐用于本地文件复制)。

COPY package.json /usr/src/app/
COPY src/ /usr/src/app/src/

3.11、VOLUME

为容器创建挂载点,用于持久化数据或共享目录

VOLUME /data

3.12、WORKDIR

设置后续指令的工作目录(相当于 cd)

WORKDIR /usr/src/app
RUN pwd   # 此时工作目录为 /usr/src/app

3.13、USER

指定运行后续指令的用户(用户名或 UID)

USER node
RUN whoami   # 输出 node

3.14、ARG

定义构建参数,可通过 --build-arg 传入

ARG VERSION=latest
FROM alpine:$VERSION

3.15、ONBUILD

当该镜像被用作其他 Dockerfile 的基础镜像时,触发后续指令。

ONBUILD COPY . /app/src
ONBUILD RUN make /app/src

3.16、STOPSIGNAL

设置停止容器时发送的系统调用信号。

STOPSIGNAL SIGQUIT

3.17、HEALTHCHECK

定义健康检查命令,Docker 会定期执行并判断容器状态。

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

3.18、SHELL

覆盖默认的 shell(Linux 默认为 [“/bin/sh”, “-c”],Windows 默认为 [“cmd”, “/S”, “/C”])。

SHELL ["/bin/bash", "-c"]
RUN echo "使用 bash 执行"

四、如何使用

编写 Dockerfile
在项目根目录创建一个名为 Dockerfile(无扩展名)的文件,写入指令。例如一个简单的 Python Web 应用的 Dockerfile(以测试环境为例):

# 使用官方 Python 3.9 镜像作为基础
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app\

# 复制依赖文件
COPY requirements.txt .

# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 声明容器运行时监听的端口(Flask 默认 5000)
EXPOSE 5000

# 设置环境变量(可选)
ENV FLASK_APP=app.py
ENV FLASK_ENV=development

# 容器启动命令
CMD ["flask", "run", "--host=0.0.0.0"]

  1. 构建镜像
    在 Dockerfile 所在目录执行以下命令,-t 用于给镜像命名(标签):
  docker build -t my-python-app .

Docker 会读取当前目录(上下文)中的 Dockerfile 并执行构建。

  1. 运行容器
    基于构建的镜像创建并启动容器:
    docker run -p 5000:5000 my-python-app
    
    这样就将容器内的 5000 端口映射到本机的 5000 端口,可以访问应用了。

备注

1、什么是过多无意义的层会造成镜像膨胀过大?

在 Docker 中,镜像是由一系列只读层(Layer)叠加而成的。每个 Dockerfile 指令(如 RUN、COPY、ADD 等)在执行时都会创建一个新的层。理解这一点对优化镜像大小非常重要。

分层存储原理

  • 层(Layer):每个指令执行后,文件系统的变化会被记录为一个新层。这些层是只读的,并堆叠在一起,构成最终的镜像。
  • 缓存与复用:由于层是独立的,Docker 可以缓存每一层。如果某层没有变化,后续构建可以直接使用缓存,加快构建速度。
  • 联合文件系统(UnionFS):Docker 使用联合文件系统(如 overlay2)将这些层组合成一个统一的文件系统视图。

原因
1、每一层都会保存文件系统的变化
即使你在后续层中删除了某个文件,该文件实际上仍然存在于之前的层中。Docker 只是在新的层中标记该文件为“已删除”,但底层文件依然占据空间。例如:

FROM ubuntu:20.04
RUN apt update && apt install -y large-package   # 层1:安装 large-package,产生大量文件
RUN apt remove -y large-package                   # 层2:删除 large-package

结果:镜像仍然包含 large-package 的文件,因为层1永久保存了它们,层2只是添加了一个删除标记(在联合文件系统中,上层文件会隐藏下层同名文件,但下层文件仍存在于镜像中)。最终镜像大小几乎等于安装 large-package 后的体积。

2、每个层本身是独立的文件
每个层都是存储在磁盘上的文件或目录。层数越多,元数据开销也越大,虽然单个层可能很小,但累积起来也会占用额外空间。

3、临时文件或中间产物残留
如果在同一层中创建临时文件并在下一层删除,这些文件仍然占据空间。例如:

RUN wget https://example.com/bigfile.tar.gz   # 层1:下载大文件
RUN tar -xzf bigfile.tar.gz                    # 层2:解压,但上一层的 tar.gz 文件仍然存在
RUN rm bigfile.tar.gz                           # 层3:删除 tar.gz,但文件依然在层1中

正确做法是将下载、解压、删除放在同一个 RUN 指令中,这样所有操作都在一层内完成,中间文件不会保留到最终镜像。

如何避免层膨胀
1、合并 RUN 指令

RUN apt update && \
    apt install -y package1 package2 && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

这样所有操作都在同一层,中间产生的临时文件(如 apt 缓存)在该层结束时被清理,不会增加额外层。

2、使用多阶段构建
对于编译型语言(如 Go、Java),可以在一个阶段编译,然后将编译产物复制到最终镜像,避免携带编译工具链和源码。

# 构建阶段
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 最终阶段
FROM alpine:latest
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

3、谨慎使用 COPY 和 ADD
每次 COPY 都会创建新层,尽量将需要复制的文件合并复制(如一次性复制整个目录),减少层数。

4、使用 .dockerignore
排除不必要的文件进入构建上下文,避免它们被添加到镜像中(尽管它们不会产生层,但会影响构建速度和上下文大小)

更多推荐