1. 引言

Dockerfile 是一个文本文件,其中包含了一系列用于构建 Docker 镜像的指令。通过编写 Dockerfile,我们可以定义镜像的构建过程,实现应用环境的标准化和自动化部署。掌握 Dockerfile 的常用指令是高效使用 Docker 的基础。本文将详细介绍 Dockerfile 的核心指令及其用法。

2. 基础指令

FROM

FROM 指令用于指定基础镜像,所有后续的指令都基于这个镜像进行构建。它是 Dockerfile 中必须且应该是第一条的指令(注释除外)。

语法:

FROM <image>[:<tag>] [AS <name>]

示例:

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

# 使用 Alpine Linux 版本,镜像更小
FROM alpine:latest AS builder

RUN

RUN 指令用于在镜像构建过程中执行命令。每一条 RUN 指令都会在当前镜像的顶层创建一个新的层并提交。

语法:

# Shell 格式(默认在 /bin/sh -c 下执行)
RUN <command>

# Exec 格式(推荐,避免 shell 解析问题)
RUN ["executable", "param1", "param2"]

示例:

# 更新包列表并安装 curl
RUN apt-get update && apt-get install -y curl

# 使用 Exec 格式执行命令
RUN ["/bin/bash", "-c", "echo 'Hello from exec format'"]

CMD

CMD 指令用于指定容器启动时默认执行的命令。一个 Dockerfile 中只能有一条 CMD 指令,如果有多条则只有最后一条生效。CMD 的主要目的是为容器提供默认的执行命令。

语法:

# Exec 格式(推荐)
CMD ["executable","param1","param2"]

# Shell 格式
CMD command param1 param2

# 作为 ENTRYPOINT 的默认参数
CMD ["param1","param2"]

示例:

# 启动一个 Python 应用
CMD ["python", "app.py"]

# 启动 Nginx 并保持前台运行
CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT 指令用于配置容器启动时运行的命令,使其成为一个可执行文件。与 CMD 不同,ENTRYPOINT 的命令不会被 docker run 后面的参数覆盖,而是将这些参数作为附加参数传递给 ENTRYPOINT 指定的命令。

语法:

# Exec 格式(推荐)
ENTRYPOINT ["executable", "param1", "param2"]

# Shell 格式
ENTRYPOINT command param1 param2

示例:

# 将容器配置为一个 `curl` 工具
ENTRYPOINT ["curl"]
CMD ["-s", "https://httpbin.org/get"]

运行 docker run myimage -I 时,实际执行的命令是 curl -I

CMD 与 ENTRYPOINT 的区别与配合使用

CMDENTRYPOINT 都是用于定义容器启动时执行的命令,但它们在功能和使用场景上有重要区别。理解这两者的差异对于编写灵活的 Dockerfile 至关重要。

核心区别
特性 CMD ENTRYPOINT
主要用途 为容器提供默认的可执行命令或参数 定义容器的主命令,使其成为一个可执行文件
可被覆盖 是,docker run 后面的参数会完全替换 CMD 否,docker run 后面的参数会作为附加参数传递给 ENTRYPOINT
Dockerfile 中数量 只能有一条(最后一条生效) 只能有一条(最后一条生效)
推荐格式 Exec 格式:CMD ["executable", "param1", "param2"] Exec 格式:ENTRYPOINT ["executable", "param1", "param2"]
使用场景对比

1. 单独使用 CMD
当容器的主要用途是运行一个可执行程序,且用户可能需要在运行时提供不同参数时使用。

# Dockerfile
FROM ubuntu:latest
CMD ["echo", "Hello, World!"]

运行示例:

# 使用默认命令
docker run myimage
# 输出: Hello, World!

# 覆盖 CMD
docker run myimage echo "Goodbye"
# 输出: Goodbye

2. 单独使用 ENTRYPOINT
当容器被设计为一个具体的工具或应用,用户只能提供参数而不能改变主命令时使用。

# Dockerfile - 创建一个 curl 工具容器
FROM alpine:latest
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]

运行示例:

# 必须使用 curl 命令,但可以添加参数
docker run mycurl -I https://example.com
# 实际执行: curl -I https://example.com

# 尝试覆盖主命令会失败
docker run mycurl wget https://example.com
# 实际执行: curl wget https://example.com (会报错)

3. CMD 与 ENTRYPOINT 配合使用(推荐)
这是最常用的模式:ENTRYPOINT 定义主命令,CMD 提供默认参数。

# Dockerfile - 创建一个灵活的 curl 工具
FROM alpine:latest
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["-s", "https://httpbin.org/get"]

运行示例:

# 使用默认参数
docker run mycurl
# 实际执行: curl -s https://httpbin.org/get

# 覆盖 CMD 的默认参数
docker run mycurl -I https://google.com
# 实际执行: curl -I https://google.com

# 使用不同参数组合
docker run mycurl -v -X POST https://api.example.com/data
# 实际执行: curl -v -X POST https://api.example.com/data
实际应用示例

示例 1:Python 应用容器

FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

# ENTRYPOINT 定义主程序,CMD 提供默认参数
ENTRYPOINT ["python"]
CMD ["app.py"]

运行方式:

# 运行默认应用
docker run myapp
# 实际执行: python app.py

# 运行其他 Python 脚本
docker run myapp other_script.py
# 实际执行: python other_script.py

# 进入 Python 交互模式
docker run -it myapp
# 实际执行: python (进入交互模式)

示例 2:数据库客户端工具

FROM mysql:8.0
ENTRYPOINT ["mysql"]
CMD ["-h", "localhost", "-u", "root", "-p"]

运行方式:

# 使用默认连接参数
docker run -it mysql-client
# 实际执行: mysql -h localhost -u root -p

# 连接到不同的数据库
docker run -it mysql-client -h db.example.com -u admin -p
# 实际执行: mysql -h db.example.com -u admin -p

示例 3:Web 服务器配置

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY html/ /usr/share/nginx/html/

# ENTRYPOINT 确保 nginx 总是以正确的方式启动
ENTRYPOINT ["nginx"]

# CMD 提供默认的运行参数
CMD ["-g", "daemon off;"]

运行方式:

# 正常启动 nginx
docker run my-nginx
# 实际执行: nginx -g "daemon off;"

# 测试 nginx 配置
docker run my-nginx -t
# 实际执行: nginx -t

# 重新加载配置
docker exec my-nginx nginx -s reload
最佳实践建议
  1. 优先使用 Exec 格式:避免 shell 解析问题,确保信号正确传递。
  2. ENTRYPOINT 用于定义"是什么":当容器是一个具体的工具或应用时使用。
  3. CMD 用于定义"做什么":当容器需要灵活的参数时使用。
  4. 组合使用实现灵活性ENTRYPOINT + CMD 的组合提供了最大的灵活性。
  5. 考虑容器用途
    • 工具类容器:使用 ENTRYPOINT 定义工具,CMD 提供常用参数
    • 应用类容器:使用 ENTRYPOINT 定义启动器,CMD 提供配置参数
    • 临时任务容器:可以只使用 CMD,方便完全覆盖
调试技巧

查看镜像的默认命令:

# 查看 CMD 和 ENTRYPOINT 配置
docker image inspect myimage --format='{{json .Config.Cmd}}'
docker image inspect myimage --format='{{json .Config.Entrypoint}}'

# 查看完整配置
docker image inspect myimage | grep -A5 -B5 "Cmd\|Entrypoint"

测试命令执行:

# 测试容器启动命令
docker run --rm --entrypoint="" myimage echo "测试"

通过合理使用 CMDENTRYPOINT,你可以创建出既灵活又专业的 Docker 镜像,满足不同场景下的需求。

3. 工作目录与文件操作指令

WORKDIR

WORKDIR 指令用于设置后续 RUNCMDENTRYPOINTCOPYADD 指令的工作目录。如果目录不存在,Docker 会自动创建它。

语法:

WORKDIR /path/to/workdir

示例:

WORKDIR /app
RUN pwd  # 输出 /app

COPY

COPY 指令用于将构建上下文中的文件或目录复制到镜像内的指定路径。

语法:

COPY [--chown=<user>:<group>] <src>... <dest>

示例:

# 复制当前目录下的所有 .py 文件到镜像的 /app 目录
COPY *.py /app/

# 复制目录并修改文件所有者
COPY --chown=node:node ./src /app/src

ADD

ADD 指令与 COPY 功能类似,但增加了两个特性:

  1. 源路径可以是 URL,Docker 会自动下载该文件。
  2. 如果源路径是一个本地压缩文件(如 .tar, .gz),ADD 会自动解压到目标路径。

语法:

ADD [--chown=<user>:<group>] <src>... <dest>

示例:

# 从 URL 下载文件
ADD https://example.com/file.tar.gz /tmp/

# 自动解压本地压缩包
ADD app.tar.gz /app/

注意: 在大多数只需要复制文件的场景下,推荐使用 COPY,因为它行为更明确。

4. 环境配置指令

ENV

ENV 指令用于设置环境变量,这些变量在构建阶段和容器运行时都可用。

语法:

# 设置单个变量
ENV <key> <value>

# 设置多个变量(推荐)
ENV <key1>=<value1> <key2>=<value2> ...

示例:

ENV NODE_ENV=production
ENV APP_VERSION=1.0.0 APP_PORT=8080

ARG

ARG 指令用于定义构建时的变量,这些变量只在 docker build 过程中有效,不会保留在最终镜像或容器运行时环境中。可以通过 --build-arg <varname>=<value> 在构建时传入值。

语法:

ARG <name>[=<default value>]

示例:

ARG USER_NAME=default_user
RUN echo "Building for user: $USER_NAME"

EXPOSE

EXPOSE 指令用于声明容器在运行时监听的网络端口。这只是一个文档性质的声明,并不会自动发布端口。实际端口映射需要在 docker run 时使用 -p 参数。

语法:

EXPOSE <port> [<port>/<protocol>...]

示例:

# 声明容器监听 80 端口(TCP)
EXPOSE 80

# 声明监听 8080 端口(TCP)和 53 端口(UDP)
EXPOSE 8080/tcp 53/udp

5. 用户与权限指令

USER

USER 指令用于指定后续 RUNCMDENTRYPOINT 指令以哪个用户身份执行。这有助于提升安全性,避免以 root 用户运行应用。

语法:

USER <user>[:<group>] or USER <UID>[:<GID>]

示例:

# 创建一个非 root 用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

VOLUME

VOLUME 指令用于创建挂载点,将容器内的目录标记为需要持久化或与宿主机共享的数据卷。即使容器被删除,卷中的数据也会保留。

语法:

VOLUME ["/path/to/volume"]

示例:

VOLUME ["/var/log", "/data"]

6. 构建优化指令

LABEL

LABEL 指令用于为镜像添加元数据,以键值对的形式存储。可以用来记录维护者信息、版本、描述等。

语法:

LABEL <key>=<value> <key>=<value> ...

示例:

LABEL maintainer="dev@example.com"
LABEL version="1.0" description="My Application"

SHELL

SHELL 指令用于覆盖 RUNCMDENTRYPOINT 指令默认使用的 shell。在 Windows 镜像中常用。

语法:

SHELL ["executable", "parameters"]

示例:

# 在 Windows 镜像中使用 PowerShell
SHELL ["powershell", "-Command"]

HEALTHCHECK

HEALTHCHECK 指令用于告诉 Docker 如何测试容器是否仍在正常工作。这可以用于实现容器健康状态检查。

语法:

HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE  # 禁用从基础镜像继承的健康检查

示例:

# 每 30 秒检查一次,超时 3 秒,连续失败 3 次标记为不健康
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

7. 综合示例

下面是一个完整的 Dockerfile 示例,它构建了一个简单的 Python Web 应用镜像,展示了多个指令的配合使用。

# 使用官方 Python 轻量级镜像作为基础
FROM python:3.9-slim AS builder

# 设置元数据
LABEL maintainer="team@example.com"
LABEL version="1.0"

# 设置构建参数
ARG APP_ENV=production

# 设置工作目录
WORKDIR /app

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    APP_ENV=${APP_ENV}

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 创建一个非 root 用户并切换
RUN useradd -m -u 1000 appuser && chown -R appuser /app
USER appuser

# 声明容器监听的端口
EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
  CMD python health_check.py

# 设置容器启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

8. 总结

Dockerfile 的指令各有其职责,理解并合理使用它们是构建高效、安全 Docker 镜像的关键。以下是一些最佳实践建议:

  1. 使用官方、轻量的基础镜像(如 -alpine, -slim 版本)。
  2. 合并 RUN 指令,减少镜像层数,并记得清理 apt 缓存等临时文件。
  3. 合理使用 .dockerignore 文件,避免将不必要的文件加入构建上下文。
  4. 明确指定版本标签,避免使用 latest 标签导致构建不确定性。
  5. 使用非 root 用户运行应用,提升容器安全性。
  6. 利用多阶段构建(Multi-stage builds)来减小最终镜像体积。

通过熟练掌握这些指令,你将能够为任何应用创建定制化、可复现的 Docker 镜像。

9. 实战演练:从编写到运行

本节将通过一个完整的实战示例,演示如何编写 Dockerfile、构建镜像并运行容器。

9.1 准备项目文件

首先创建一个简单的 Python Web 应用项目:

# 创建项目目录
mkdir docker-demo && cd docker-demo

# 创建应用文件
cat > app.py << 'EOF'
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from Docker! Hostname: {os.uname().nodename}"

@app.route('/health')
def health():
    return 'OK', 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

# 创建依赖文件
cat > requirements.txt << 'EOF'
Flask==2.3.3
EOF

# 创建健康检查脚本
cat > health_check.py << 'EOF'
import requests
try:
    resp = requests.get('http://localhost:5000/health', timeout=2)
    if resp.status_code == 200:
        exit(0)
    else:
        exit(1)
except:
    exit(1)
EOF

9.2 编写 Dockerfile

创建 Dockerfile 文件:

# 使用官方 Python 轻量级镜像
FROM python:3.9-slim

# 设置元数据
LABEL maintainer="demo@example.com"
LABEL version="1.0"
LABEL description="Flask Web App Demo"

# 设置构建参数(可在构建时覆盖)
ARG APP_ENV=production

# 设置工作目录
WORKDIR /app

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    FLASK_APP=app.py \
    APP_ENV=${APP_ENV}

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
    pip install requests  # 健康检查需要

# 复制应用代码
COPY . .

# 创建非 root 用户并切换(提升安全性)
RUN groupadd -r appgroup && useradd -r -g appgroup appuser && \
    chown -R appuser:appgroup /app
USER appuser

# 声明容器监听的端口
EXPOSE 5000

# 健康检查(每30秒检查一次)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD python health_check.py

# 设置容器启动命令
CMD ["python", "app.py"]

9.3 构建 Docker 镜像

使用 docker build 命令构建镜像:

# 构建镜像(当前目录有 Dockerfile)
docker build -t flask-demo:1.0 .

# 查看构建的镜像
docker images | grep flask-demo

# 构建时传递构建参数
docker build --build-arg APP_ENV=development -t flask-demo:dev .

构建过程说明:

  1. Docker 读取当前目录下的 Dockerfile
  2. 按照指令顺序逐层构建
  3. 每执行一条指令都会创建一个新的镜像层
  4. 最终生成名为 flask-demo:1.0 的镜像

9.4 运行 Docker 容器

构建成功后,运行容器:

# 运行容器(后台模式)
docker run -d --name my-flask-app -p 8080:5000 flask-demo:1.0

# 查看运行中的容器
docker ps

# 查看容器日志
docker logs my-flask-app

# 测试应用
curl http://localhost:8080/

# 查看容器健康状态
docker inspect --format='{{.State.Health.Status}}' my-flask-app

9.5 常用操作命令

# 进入容器内部
docker exec -it my-flask-app /bin/bash

# 停止容器
docker stop my-flask-app

# 启动已停止的容器
docker start my-flask-app

# 重启容器
docker restart my-flask-app

# 删除容器
docker rm my-flask-app

# 删除镜像
docker rmi flask-demo:1.0

# 查看镜像构建历史
docker history flask-demo:1.0

docker history 命令详解:

docker history 命令用于查看 Docker 镜像的构建历史,显示镜像每一层的详细信息。这对于调试镜像构建过程、优化镜像大小以及理解镜像的组成非常有帮助。

基本语法:

docker history [OPTIONS] IMAGE

常用选项:

  • --no-trunc:显示完整的输出,不截断信息
  • --quiet-q:只显示镜像层的 ID
  • --format:使用 Go 模板格式化输出
  • --human:以人类可读的格式显示大小(默认)
  • --no-human:以字节为单位显示大小

示例输出:

$ docker history flask-demo:1.0
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
a1b2c3d4e5f6   2 minutes ago   CMD ["python" "app.py"]                        0B        buildkit.dockerfile.v0
b2c3d4e5f6g7   2 minutes ago   USER appuser                                   0B        buildkit.dockerfile.v0
c3d4e5f6g7h8   2 minutes ago   RUN /bin/sh -c groupadd -r appgroup && use…   1.2MB     buildkit.dockerfile.v0
d4e5f6g7h8i9   2 minutes ago   COPY . . # buildkit                            15.4kB    buildkit.dockerfile.v0
e5f6g7h8i9j0   2 minutes ago   COPY requirements.txt . # buildkit             87B       buildkit.dockerfile.v0
f6g7h8i9j0k1   2 minutes ago   ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFF…   0B        buildkit.dockerfile.v0
g7h8i9j0k1l2   2 minutes ago   WORKDIR /app                                   0B        buildkit.dockerfile.v0
h8i9j0k1l2m3   2 minutes ago   ARG APP_ENV=production                          0B        buildkit.dockerfile.v0
i9j0k1l2m3n4   2 minutes ago   LABEL version=1.0 description="Flask Web A…   0B        buildkit.dockerfile.v0
j0k1l2m3n4o5   2 minutes ago   LABEL maintainer=demo@example.com              0B        buildkit.dockerfile.v0
k1l2m3n4o5p6   2 weeks ago     /bin/sh -c #(nop)  CMD ["python3"]             0B        
l2m3n4o5p6q7   2 weeks ago     /bin/sh -c #(nop)  ENV PYTHON_GET_PIP_SHA2…   0B        
...

输出字段说明:

  1. IMAGE:镜像层的 ID(前12位)
  2. CREATED:该层创建的时间
  3. CREATED BY:创建该层的 Dockerfile 指令
  4. SIZE:该层的大小
  5. COMMENT:注释信息(通常是 Dockerfile 指令的原始内容)

实用技巧:

  1. 查看完整构建命令:
docker history --no-trunc flask-demo:1.0
  1. 只显示镜像层ID:
docker history -q flask-demo:1.0
  1. 自定义格式化输出:
docker history --format "{{.ID}}\t{{.CreatedBy}}\t{{.Size}}" flask-demo:1.0
  1. 分析镜像大小:
# 查看各层大小,找出占用空间最大的层
docker history --human=false flask-demo:1.0 | sort -k4 -n
  1. 比较两个镜像的构建历史:
# 查看两个版本镜像的差异
docker history flask-demo:1.0 > history_v1.txt
docker history flask-demo:2.0 > history_v2.txt
diff history_v1.txt history_v2.txt

使用场景:

  • 调试构建问题:当镜像构建失败时,可以查看历史记录确定在哪一步出错
  • 优化镜像大小:识别哪些层占用了大量空间,考虑合并 RUN 指令或使用多阶段构建
  • 安全审计:检查镜像中是否包含敏感信息或不必要的文件
  • 理解镜像结构:了解镜像的构建过程和依赖关系

注意事项:

  • docker history 显示的是镜像的层历史,而不是容器的运行历史
  • 对于使用多阶段构建的镜像,只会显示最终阶段的层历史
  • 某些层可能被标记为 <missing>,这通常是因为基础镜像的层不在本地

9.6 使用 .dockerignore 优化构建

创建 .dockerignore 文件避免不必要的文件被复制到镜像中:

# 忽略 Git 相关文件
.git/
.gitignore

# 忽略 Python 缓存和虚拟环境
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/

# 忽略日志和临时文件
*.log
*.tmp
*.temp

# 忽略 IDE 配置文件
.vscode/
.idea/
*.swp
*.swo

# 忽略本地配置文件
.env
.env.local

9.7 多阶段构建示例(优化镜像体积)

对于生产环境,可以使用多阶段构建来减小最终镜像体积:

# 第一阶段:构建阶段
FROM python:3.9 AS builder

WORKDIR /app

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

# 安装依赖到虚拟环境
RUN python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

# 第二阶段:运行阶段
FROM python:3.9-slim

WORKDIR /app

# 从构建阶段复制虚拟环境
COPY --from=builder /opt/venv /opt/venv

# 设置 PATH 使用虚拟环境的 Python
ENV PATH="/opt/venv/bin:$PATH"

# 复制应用代码
COPY . .

# 创建非 root 用户
RUN useradd -m -u 1000 appuser && chown -R appuser /app
USER appuser

EXPOSE 5000

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

9.8 验证与调试

如果遇到问题,可以使用以下命令进行调试:

# 查看详细的构建过程
docker build --progress=plain -t flask-demo:debug .

# 运行临时容器进行测试
docker run --rm -it --entrypoint /bin/bash flask-demo:1.0

# 检查镜像层信息
docker image inspect flask-demo:1.0

# 查看容器资源使用情况
docker stats my-flask-app

# 查看容器内部进程
docker top my-flask-app

9.9 总结

通过这个完整的实战演练,你学会了:

  1. 编写 Dockerfile:使用各种指令定义镜像构建过程
  2. 构建镜像:使用 docker build 命令创建自定义镜像
  3. 运行容器:使用 docker run 启动应用容器
  4. 日常操作:掌握容器的启动、停止、查看日志等基本操作
  5. 优化技巧:使用 .dockerignore 和多阶段构建优化镜像

建议在实际项目中从简单开始,逐步添加更多指令和优化,最终形成适合自己项目的 Dockerfile 最佳实践。

更多推荐