Dockerfile 常用指令详解
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 的区别与配合使用
CMD 和 ENTRYPOINT 都是用于定义容器启动时执行的命令,但它们在功能和使用场景上有重要区别。理解这两者的差异对于编写灵活的 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
最佳实践建议
- 优先使用 Exec 格式:避免 shell 解析问题,确保信号正确传递。
- ENTRYPOINT 用于定义"是什么":当容器是一个具体的工具或应用时使用。
- CMD 用于定义"做什么":当容器需要灵活的参数时使用。
- 组合使用实现灵活性:
ENTRYPOINT+CMD的组合提供了最大的灵活性。 - 考虑容器用途:
- 工具类容器:使用
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 "测试"
通过合理使用 CMD 和 ENTRYPOINT,你可以创建出既灵活又专业的 Docker 镜像,满足不同场景下的需求。
3. 工作目录与文件操作指令
WORKDIR
WORKDIR 指令用于设置后续 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令的工作目录。如果目录不存在,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 功能类似,但增加了两个特性:
- 源路径可以是 URL,Docker 会自动下载该文件。
- 如果源路径是一个本地压缩文件(如
.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 指令用于指定后续 RUN、CMD 和 ENTRYPOINT 指令以哪个用户身份执行。这有助于提升安全性,避免以 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 指令用于覆盖 RUN、CMD 和 ENTRYPOINT 指令默认使用的 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 镜像的关键。以下是一些最佳实践建议:
- 使用官方、轻量的基础镜像(如
-alpine,-slim版本)。 - 合并 RUN 指令,减少镜像层数,并记得清理 apt 缓存等临时文件。
- 合理使用
.dockerignore文件,避免将不必要的文件加入构建上下文。 - 明确指定版本标签,避免使用
latest标签导致构建不确定性。 - 使用非 root 用户运行应用,提升容器安全性。
- 利用多阶段构建(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 .
构建过程说明:
- Docker 读取当前目录下的
Dockerfile - 按照指令顺序逐层构建
- 每执行一条指令都会创建一个新的镜像层
- 最终生成名为
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
...
输出字段说明:
- IMAGE:镜像层的 ID(前12位)
- CREATED:该层创建的时间
- CREATED BY:创建该层的 Dockerfile 指令
- SIZE:该层的大小
- COMMENT:注释信息(通常是 Dockerfile 指令的原始内容)
实用技巧:
- 查看完整构建命令:
docker history --no-trunc flask-demo:1.0
- 只显示镜像层ID:
docker history -q flask-demo:1.0
- 自定义格式化输出:
docker history --format "{{.ID}}\t{{.CreatedBy}}\t{{.Size}}" flask-demo:1.0
- 分析镜像大小:
# 查看各层大小,找出占用空间最大的层
docker history --human=false flask-demo:1.0 | sort -k4 -n
- 比较两个镜像的构建历史:
# 查看两个版本镜像的差异
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 总结
通过这个完整的实战演练,你学会了:
- 编写 Dockerfile:使用各种指令定义镜像构建过程
- 构建镜像:使用
docker build命令创建自定义镜像 - 运行容器:使用
docker run启动应用容器 - 日常操作:掌握容器的启动、停止、查看日志等基本操作
- 优化技巧:使用
.dockerignore和多阶段构建优化镜像
建议在实际项目中从简单开始,逐步添加更多指令和优化,最终形成适合自己项目的 Dockerfile 最佳实践。
更多推荐
所有评论(0)