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
排除不必要的文件进入构建上下文,避免它们被添加到镜像中(尽管它们不会产生层,但会影响构建速度和上下文大小)

更多推荐