Dockerfile 教程
什么是 Dockerfile?
Dockerfile 是一个文本文件,它包含了一系列用户可以调用命令行来自动构建 Docker 镜像的指令。简单来说,Dockerfile 就是构建镜像的“说明书”或“配方”。通过 docker build
命令,Docker 可以读取 Dockerfile 中的指令,并按顺序执行,最终生成一个自定义的 Docker 镜像。
Dockerfile 基本结构和常用指令
一个典型的 Dockerfile 通常包含以下几个部分,并按特定顺序组织:
FROM
- 指定基础镜像:- 每个 Dockerfile 的第一条非注释指令必须是
FROM
。 - 它指定了你构建新镜像所依赖的基础镜像。例如,如果你要构建一个运行 Python 应用的镜像,可能会选择一个官方的 Python 镜像作为基础。
- 语法:
FROM <image>:<tag>
或FROM <image>@<digest>
- 示例:
FROM python:3.9-slim
或FROM ubuntu:22.04
- 每个 Dockerfile 的第一条非注释指令必须是
LABEL
- 添加元数据 (可选):- 用于为镜像添加元数据,如维护者信息、版本号等。
- 语法:
LABEL <key>=<value> <key>=<value> ...
- 示例:
LABEL maintainer="Your Name <your.email@example.com>"
ARG
- 定义构建时变量 (可选):- 定义在
docker build
命令执行时可以传递给构建过程的变量。注意,ARG
定义的变量在镜像构建完成后通常不可用,除非它被ENV
指令使用。 - 语法:
ARG <name>[=<default value>]
- 示例:
ARG APP_VERSION=1.0
- 定义在
ENV
- 设置环境变量:- 设置环境变量。这些变量在镜像构建过程中以及基于该镜像启动的容器中都可用。
- 语法:
ENV <key>=<value>
或ENV <key1>=<value1> <key2>=<value2> ...
- 示例:
ENV PYTHONDONTWRITEBYTECODE=1
(阻止 Python 生成 .pyc 文件) - 示例:
ENV APP_HOME=/app
WORKDIR
- 设置工作目录:- 为后续的
RUN
,CMD
,ENTRYPOINT
,COPY
,ADD
指令设置工作目录。如果目录不存在,WORKDIR
会自动创建它。 - 推荐使用绝对路径。
- 语法:
WORKDIR /path/to/workdir
- 示例:
WORKDIR /app
(后续指令将在/app
目录下执行)
- 为后续的
COPY
/ADD
- 复制文件或目录:COPY
: 将构建上下文(通常是 Dockerfile 所在的目录及其子目录)中的文件或目录复制到镜像的文件系统中。- 语法:
COPY [--chown=<user>:<group>] <src>... <dest>
- 示例:
COPY . .
(将当前目录所有内容复制到镜像的当前工作目录) - 示例:
COPY requirements.txt /app/
- 语法:
ADD
: 功能与COPY
类似,但增加了两个额外功能:- 源
<src>
可以是 URL。 - 如果源
<src>
是一个可识别的压缩格式(如 tar, gzip, bzip2, xz),它会被自动解压缩到目标<dest>
。 - 注意: 除非明确需要
ADD
的特殊功能(如解压或下载 URL),否则官方推荐优先使用COPY
,因为它的行为更明确、可预测。 - 语法:
ADD [--chown=<user>:<group>] <src>... <dest>
- 示例:
ADD https://example.com/file.tar.gz /tmp/
(下载并解压)
- 源
RUN
- 执行构建命令:- 在镜像构建过程中执行指定的命令。通常用于安装软件包、创建目录、编译代码等。
- 每条
RUN
指令都会在当前镜像的顶层创建一个新的层。为了减少镜像层数,通常建议将多个相关的命令合并到一条RUN
指令中,使用&&
连接。 - 有两种格式:
- Shell 格式:
RUN <command>
(命令在 shell 中执行,通常是/bin/sh -c
) - Exec 格式:
RUN ["executable", "param1", "param2"]
(直接执行,不通过 shell)
- Shell 格式:
- 示例(Shell 格式):
1
2
3
4RUN apt-get update && apt-get install -y --no-install-recommends \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/* - 示例(Exec 格式,较少用于
RUN
):1
RUN ["apt-get", "update"]
EXPOSE
- 声明端口:- 声明容器在运行时打算监听的网络端口。这不会实际发布端口,只是作为文档告诉用户(或自动化工具)哪些端口是有意暴露的。
- 实际发布端口需要在
docker run
时使用-p
或-P
参数。 - 语法:
EXPOSE <port> [<port>/<protocol>...]
(默认协议是 TCP) - 示例:
EXPOSE 8080
或EXPOSE 80/tcp 53/udp
VOLUME
- 定义匿名卷 (可选):- 创建一个指定名称的挂载点,并将其标记为持有来自外部挂载的卷或来自其他容器的卷。常用于数据库存储、日志文件等需要持久化或共享的数据。
- 语法:
VOLUME ["/path/to/volume"]
- 示例:
VOLUME ["/var/log", "/data"]
USER
- 设置运行用户 (可选):- 指定运行后续
RUN
,CMD
,ENTRYPOINT
指令时使用的用户名或 UID(以及可选的组名或 GID)。 - 为了安全起见,推荐在不需要 root 权限时切换到非 root 用户。
- 语法:
USER <user>[:<group>]
或USER <UID>[:<GID>]
- 示例:
RUN useradd -ms /bin/bash myuser
- 示例:
USER myuser
- 指定运行后续
ENTRYPOINT
- 配置容器启动时执行的命令:- 允许你将容器配置为像可执行文件一样运行。
ENTRYPOINT
指定的命令不容易在容器启动时被覆盖(除非使用docker run --entrypoint
)。- 通常与
CMD
配合使用,CMD
提供ENTRYPOINT
的默认参数。 - 有两种格式:
- Exec 格式(推荐):
ENTRYPOINT ["executable", "param1", "param2"]
- Shell 格式:
ENTRYPOINT command param1 param2
(会在/bin/sh -c
中执行,可能导致无法接收信号和 PID 1 问题,一般不推荐)
- Exec 格式(推荐):
- 示例(Exec 格式):
ENTRYPOINT ["python", "app.py"]
CMD
- 提供容器启动的默认命令或参数:CMD
指令有三种用途:- 为
ENTRYPOINT
指令提供默认参数。 - 如果 Dockerfile 中没有
ENTRYPOINT
,则CMD
指定容器启动时要执行的默认命令。 - 如果在
docker run
命令中指定了其他命令,CMD
的值会被覆盖。
- 为
- 一个 Dockerfile 中只能有一条
CMD
指令生效。如果有多条,只有最后一条会生效。 - 有三种格式:
- Exec 格式(推荐,尤其是在没有
ENTRYPOINT
或作为ENTRYPOINT
参数时):CMD ["executable","param1","param2"]
- 作为
ENTRYPOINT
的默认参数:CMD ["param1","param2"]
- Shell 格式:
CMD command param1 param2
- Exec 格式(推荐,尤其是在没有
- 示例(配合
ENTRYPOINT
):1
2ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
CMD ["-c", "/etc/nginx/nginx.conf"] # 提供默认配置文件参数 - 示例(独立使用,提供默认执行命令):
1
CMD ["python", "app.py"]
- 示例(Shell 格式):
1
CMD echo "Hello Docker"
.dockerignore
文件:- 这不是 Dockerfile 的一部分,但在构建镜像时非常重要。
- 它是一个位于构建上下文根目录(通常和 Dockerfile 在同一目录)的文件,用于指定在执行
docker build
时哪些文件或目录不应被发送到 Docker 守护进程(即不包含在构建上下文中)。 - 语法类似于
.gitignore
。 - 这有助于减小构建上下文的大小,加快构建速度,并避免将敏感信息或不必要的文件复制到镜像中。
- 示例
.dockerignore
内容:1
2
3
4
5.git
node_modules
*.log
Dockerfile
.dockerignore
编写 Dockerfile 的最佳实践
保持镜像尽可能小:
- 选择合适的基础镜像(如
alpine
,slim
版本)。 - 只安装必要的软件包。
- 清理不必要的文件,例如在
RUN
指令的最后清理包管理器的缓存(如apt-get clean
,rm -rf /var/lib/apt/lists/*
)。 - 使用多阶段构建(Multi-stage builds)。
- 选择合适的基础镜像(如
利用构建缓存:
- Docker 会缓存
RUN
,COPY
,ADD
等指令的结果层。 - 将不经常变化的指令(如安装基础依赖)放在 Dockerfile 的前面。
- 将经常变化的指令(如
COPY
应用程序代码)放在后面。 - 例如,先
COPY
依赖文件(如requirements.txt
或package.json
)并安装依赖,然后再COPY
整个应用程序代码。
- Docker 会缓存
合并
RUN
指令:- 每条
RUN
指令都会创建一个新的镜像层。过多的层会增加镜像大小。 - 使用
&&
将多个命令连接在一条RUN
指令中。
- 每条
使用
.dockerignore
:- 排除不需要发送到 Docker 守护进程的文件和目录。
优先使用
COPY
而不是ADD
:- 除非你需要
ADD
的特定功能(URL 下载或自动解压),否则COPY
更清晰、更可预测。
- 除非你需要
使用多阶段构建 (Multi-stage Builds):
- 对于需要编译步骤的应用程序(如 Go, Java, C++),可以在一个阶段(构建阶段)进行编译和构建,然后在另一个更小的阶段(运行阶段)只复制编译后的产物和必要的运行时依赖。这可以显著减小最终镜像的大小。
- 示例结构:
1
2
3
4
5
6
7
8
9
10
11
12
13# ---- Build Stage ----
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# ---- Runtime Stage ----
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/myapp .
# Copy other necessary files like configs if needed
# COPY --from=builder /app/config.yaml .
CMD ["./myapp"]
使用
Exec
格式的CMD
和ENTRYPOINT
:- 避免使用 Shell 格式,因为它会隐式启动一个 shell 进程 (
/bin/sh -c
),这可能导致信号处理问题(例如,docker stop
可能无法正常停止应用)并且你的应用不会是 PID 1。
- 避免使用 Shell 格式,因为它会隐式启动一个 shell 进程 (
以非 Root 用户运行:
- 增加安全性,避免容器内的进程拥有宿主机的 root 权限。使用
USER
指令切换用户。
- 增加安全性,避免容器内的进程拥有宿主机的 root 权限。使用
明确指定包版本:
- 在
RUN
指令中安装软件包时,尽可能指定版本号,以确保构建的可重复性。
- 在
添加
LABEL
:- 提供镜像的元数据,方便管理和理解。
简单示例:一个 Python Flask 应用的 Dockerfile
假设你的项目结构如下:
1 | . |
requirements.txt
:
1 | Flask==2.1.2 |
app.py
:
1 | from flask import Flask |
Dockerfile
:
1 | # 1. 使用官方 Python 运行时作为父镜像 |
如何构建镜像
在包含 Dockerfile
和你的应用程序代码的目录下,打开终端,运行以下命令:
1 | # -t 参数用于给镜像打标签 (name:tag) |
构建成功后,你就可以使用这个镜像来运行容器了:
1 | # -p 参数将宿主机的 8080 端口映射到容器的 5000 端口 |
现在,访问 http://localhost:8080
应该就能看到 “Hello, Docker!” 了。