Dockerfile 语法与结构
Dockerfile 是一个纯文本文件,里面包含了你平时在命令行中组装镜像时会用到的所有命令。通过运行 docker build 命令,你可以让 Docker 自动按顺序执行 Dockerfile 里的这些指令,从而快速、自动化地构建出一个全新的 Docker 镜像。
对于容器化开发来说,Dockerfile 是不可或缺的。它保证了镜像构建过程的一致性和可重复性,是你定义应用程序运行环境和依赖项的基础。想要构建出体积小、效率高且易于维护的 Docker 镜像,深入理解 Dockerfile 的语法和结构是必经之路。
1. Dockerfile 结构与语法
Dockerfile 由一行行的指令组成。Docker 会从上到下逐行执行这些指令来构建镜像。指令本身是不区分大小写的,但为了方便与普通的参数区分开来,业界约定俗成的规范是将指令全部大写。
一条 Dockerfile 指令的通用格式是:INSTRUCTION 参数 (即 指令 参数)。
1.1 基本结构
一个标准的 Dockerfile 通常遵循以下编写顺序:
- 基础镜像 (Base Image): 使用
FROM指令作为开头,指定你的镜像基于哪个现有镜像构建。 - 元数据 (Metadata): 使用
LABEL指令为镜像添加版本、作者等说明信息。 - 执行命令 (Commands): 包含一系列指令,用于安装软件、复制文件、设置环境变量等。
- 入口/默认命令 (Entrypoint/Command): 定义容器启动时默认运行的应用程序。
1.2 核心必备指令
以下是你编写 Dockerfile 时最常打交道的指令:
- FROM: 为后续指令设置基础镜像。它是整个镜像的地基。
- 示例:
FROM ubuntu:20.04(使用 Ubuntu 20.04 作为基础镜像) - 示例:
FROM node:16-alpine(基于轻量级的 Alpine Linux 使用 Node.js 16 版本) - LABEL: 以键值对(key-value)的形式为镜像添加元数据信息。
- 示例:
LABEL maintainer="your_email@example.com" - 示例:
LABEL version="1.0" description="我的超棒应用" - RUN: 在当前镜像之上创建一个新的层(layer)来执行命令,并提交结果。通常用于安装软件包、创建目录或执行系统级操作。
- 示例:
RUN apt-get update && apt-get install -y curl(更新软件包列表并安装 curl) - 示例:
RUN mkdir /app(创建一个名为 "app" 的目录) - COPY: 将本地文件系统中的文件或目录(
<源路径>)复制到镜像内的指定路径(<目标路径>)。 - 示例:
COPY ./app /app(将当前目录下的 "app" 文件夹复制到镜像内的 "/app" 目录下) - 示例:
COPY config.json /app/config.json(复制单个文件) - ADD: 功能类似于 COPY,但带有“魔法”附加功能:它可以自动解压压缩包,还能直接从网络 URL 下载文件。新手建议:除非明确需要这些高级功能,否则优先使用
COPY,因为它的行为更透明。 - 示例:
ADD app.tar.gz /app(将app.tar.gz自动解压到镜像的/app目录中) - 示例:
ADD https://example.com/file.txt /app/file.txt(从指定网址下载文件) - WORKDIR: 设置工作目录。它会影响它之后出现的所有
RUN、CMD、ENTRYPOINT、COPY和ADD指令。 - 示例:
WORKDIR /app(将工作目录切换为/app) - 示例:
WORKDIR /app/src(将工作目录切换为/app/src) - EXPOSE: 声明容器在运行时打算监听的网络端口。注意:这只是一个声明,它并不会真正在宿主机上开放端口,真正的端口映射需要在运行容器时使用
-p参数来完成。 - 示例:
EXPOSE 80(声明容器将监听 80 端口) - 示例:
EXPOSE 8080 443(声明容器将监听 8080 和 443 端口) - ENV: 设置环境变量。
- 示例:
ENV MY_NAME="John Doe" - 示例:
ENV APP_HOME /app - CMD: 为启动的容器提供默认的执行命令。一个 Dockerfile 中只能有一条
CMD指令。如果写了多条,只有最后一条会生效。 - 示例:
CMD ["可执行文件","参数1","参数2"](推荐的 JSON 数组格式) - 示例:
CMD ["/bin/bash"](启动一个 bash 终端) - ENTRYPOINT: 将容器配置为像可执行程序一样运行。它通常用于定义容器的主命令,配合 CMD 使用可以接收额外参数。
- 示例:
ENTRYPOINT ["/usr/local/bin/my-app"] - 示例:
ENTRYPOINT ["/bin/sh", "-c", "echo 'Hello, world!'"] - VOLUME: 创建一个具有指定名称的挂载点,用于将宿主机或其他容器的目录挂载到该容器中,实现数据持久化。
- 示例:
VOLUME /data(创建一个名为 "data" 的挂载卷) - USER: 指定运行镜像时(以及后续的
RUN、CMD和ENTRYPOINT指令)使用的用户名或用户 ID (UID)。 - 示例:
USER daemon - 示例:
USER 1001 - ARG: 定义构建变量。用户可以在执行
docker build命令时,通过--build-arg <变量名>=<值>传递参数。 - 示例:
ARG version=1.0 - 示例:
ARG repo_url - STOPSIGNAL: 设置发送给容器以使其退出的系统调用信号。
- 示例:
STOPSIGNAL SIGTERM - HEALTHCHECK: 告诉 Docker 如何测试容器,以验证它是否仍在正常工作。
- 示例:
HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1 - SHELL: 允许你覆盖默认的 shell(主要用于 shell 格式的命令)。
- 示例:
SHELL ["/bin/bash", "-c"]
2. 指令执行顺序与多行书写
2.1 指令顺序与缓存机制
Dockerfile 中指令的书写顺序对镜像构建的速度有着决定性的影响。Docker 使用了一种缓存机制 (Caching) 来加速构建过程。
Dockerfile 中的每一条指令都会在镜像中创建一个新的“层(Layer)”。如果在下一次构建时,Docker 发现某一条指令以及它之前的所有层都没有发生变化,它就会直接复用之前缓存的层,而不会重新执行这条指令。
最佳实践:
为了最大化利用缓存,你应该按照以下顺序排列指令:
- 极少改动的指令放在前面 (例如:安装操作系统的基础软件包)。
- 经常改动的指令放在后面 (例如:复制你正在频繁修改的应用源代码)。
这样一来,Docker 就可以复用大部分的基础层,只重新构建那些代码发生改变的层,从而大大缩短构建时间。
2.2 多行指令书写
为了让代码更具可读性,当一条命令太长时,你可以使用反斜杠 \ 将其拆分到多行。
示例:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
vim3. 实战案例演示
让我们来看看在不同场景下,如何编写实际的 Dockerfile。
3.1 案例一:简单的 Node.js 应用
这个 Dockerfile 用于打包一个简单的 Node.js 应用程序。
# 使用官方的 Node.js 运行环境作为基础镜像
FROM node:16-alpine
# 将容器内的工作目录设置为 /app
WORKDIR /app
# 将 package.json 和 package-lock.json 复制到工作目录
COPY package*.json ./
# 安装应用程序依赖
RUN npm install
# 将当前目录下的所有应用源代码复制到工作目录
COPY . .
# 声明暴露 3000 端口,以便外部可以访问
EXPOSE 3000
# 定义容器启动时运行的默认命令
CMD ["npm", "start"]解析:
FROM node:16-alpine: 使用基于 Alpine Linux 构建的轻量级 Node.js 16 镜像,能有效减小镜像体积。- 我们先
COPY package*.json然后RUN npm install,最后才COPY . .,这正是利用了缓存机制。因为依赖配置(package.json)通常比源代码更新得慢,这样做可以避免每次改动代码都要重新下载依赖。
3.2 案例二:Python Flask 应用
这个 Dockerfile 用于打包一个 Python Flask 应用程序。
# 使用官方的 Python 运行环境作为基础镜像
FROM python:3.9-slim-buster
# 将工作目录设置为 /app
WORKDIR /app
# 将依赖需求文件复制到工作目录
COPY requirements.txt .
# 安装应用程序依赖
RUN pip install --no-cache-dir -r requirements.txt
# 将应用源代码复制到工作目录
COPY . .
# 声明暴露 5000 端口
EXPOSE 5000
# 为 Flask 设置环境变量
ENV FLASK_APP=app.py
# 定义启动应用的命令
CMD ["flask", "run", "--host=0.0.0.0"]解析:
RUN pip install --no-cache-dir ...: 使用--no-cache-dir选项可以让 pip 不保存下载的缓存文件,这有助于进一步减小最终生成的镜像大小。--host=0.0.0.0: 这个配置让 Flask 应用不仅在容器内部监听,还能接受来自容器外部的访问请求。
3.3 案例三:使用 Nginx 构建静态网站
这个 Dockerfile 将静态网页文件放入 Nginx 服务器中进行托管。
# 使用官方的 Nginx 运行环境作为基础镜像
FROM nginx:stable-alpine
# 删除默认的 Nginx 配置文件
RUN rm -rf /etc/nginx/conf.d/*
# 将我们自定义的 Nginx 配置文件复制进去
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 将静态网站文件复制到 Nginx 默认的文档根目录
COPY html /usr/share/nginx/html
# 声明暴露 80 端口
EXPOSE 80
# Nginx 镜像本身自带了启动命令,因此这里不需要再写 CMD关于 nginx.conf 配置文件:
为了配合上面的 Dockerfile,你的工作目录下需要有一个 nginx.conf 文件,内容示例如下:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}这不仅配置了监听 80 端口,还指定了网页文件所在的根目录(/usr/share/nginx/html)。