Docker 教程

Dockerfile 语法与结构

Dockerfile 是一个纯文本文件,里面包含了你平时在命令行中组装镜像时会用到的所有命令。通过运行 docker build 命令,你可以让 Docker 自动按顺序执行 Dockerfile 里的这些指令,从而快速、自动化地构建出一个全新的 Docker 镜像。

对于容器化开发来说,Dockerfile 是不可或缺的。它保证了镜像构建过程的一致性和可重复性,是你定义应用程序运行环境和依赖项的基础。想要构建出体积小、效率高且易于维护的 Docker 镜像,深入理解 Dockerfile 的语法和结构是必经之路。

1. Dockerfile 结构与语法

Dockerfile 由一行行的指令组成。Docker 会从上到下逐行执行这些指令来构建镜像。指令本身是不区分大小写的,但为了方便与普通的参数区分开来,业界约定俗成的规范是将指令全部大写

一条 Dockerfile 指令的通用格式是:INSTRUCTION 参数 (即 指令 参数)。

1.1 基本结构

一个标准的 Dockerfile 通常遵循以下编写顺序:

  1. 基础镜像 (Base Image): 使用 FROM 指令作为开头,指定你的镜像基于哪个现有镜像构建。
  2. 元数据 (Metadata): 使用 LABEL 指令为镜像添加版本、作者等说明信息。
  3. 执行命令 (Commands): 包含一系列指令,用于安装软件、复制文件、设置环境变量等。
  4. 入口/默认命令 (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: 设置工作目录。它会影响它之后出现的所有 RUNCMDENTRYPOINTCOPYADD 指令。
    • 示例: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: 指定运行镜像时(以及后续的 RUNCMDENTRYPOINT 指令)使用的用户名或用户 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 发现某一条指令以及它之前的所有层都没有发生变化,它就会直接复用之前缓存的层,而不会重新执行这条指令。

最佳实践:
为了最大化利用缓存,你应该按照以下顺序排列指令:

  1. 极少改动的指令放在前面 (例如:安装操作系统的基础软件包)。
  2. 经常改动的指令放在后面 (例如:复制你正在频繁修改的应用源代码)。

这样一来,Docker 就可以复用大部分的基础层,只重新构建那些代码发生改变的层,从而大大缩短构建时间。

2.2 多行指令书写

为了让代码更具可读性,当一条命令太长时,你可以使用反斜杠 \ 将其拆分到多行。

示例:

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    curl \
    wget \
    vim

3. 实战案例演示

让我们来看看在不同场景下,如何编写实际的 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)。