Docker 系列 | 04 - 构建你的专属应用:Dockerfile 深度解析
Docker 系列 | 04 - 构建你的专属应用:Dockerfile 深度解析
引言
在之前的文章中,我们了解了 Docker 镜像和容器的基本概念,并掌握了如何运行和管理容器。但是,我们如何将自己的应用程序打包成一个可用的 Docker 镜像呢?答案就是 Dockerfile。
Dockerfile 是一个简单的文本文件,其中包含了一系列指令,Docker 引擎会按照这些指令一步步地构建镜像。它是实现“一次构建,处处运行”理念的关键,也是容器化应用程序的标准方式。
本篇文章将详细解析 Dockerfile 的常用指令,并通过示例带您亲手构建一个定制化的 Docker 镜像。
什么是 Dockerfile?
Dockerfile 是一个用于自动化构建 Docker 镜像的脚本文件。它包含了一系列指令,每条指令都对应着镜像构建过程中的一个操作,并会创建一个新的镜像层。
通过 Dockerfile,您可以:
- 定义基础镜像:您的应用将运行在哪个操作系统或基础环境之上。
- 添加文件:将您的应用程序代码、配置文件等拷贝到镜像中。
- 安装依赖:安装应用程序所需的运行时、库和工具。
- 暴露端口:声明容器将对外开放哪些端口。
- 定义启动命令:指定容器启动时执行的默认命令。
Dockerfile 常用指令详解
以下是 Dockerfile 中一些最常用和最重要的指令:
1. FROM
- 作用:指定新镜像的基础镜像。Dockerfile 的第一条指令必须是
FROM
。 - 语法:
FROM <image>[:<tag>]
- 示例:
1
2FROM ubuntu:22.04 # 基于 Ubuntu 22.04 LTS
FROM python:3.9-slim-buster # 基于 Python 3.9 的轻量级 Debian Buster
2. RUN
- 作用:在镜像构建过程中执行命令,并将结果作为新层提交。适用于安装软件包、创建文件、执行脚本等。
- 语法:
RUN <command>
(shell 格式,命令在/bin/sh -c
中运行)RUN ["executable", "param1", "param2"]
(exec 格式,直接执行可执行文件,不会经过 shell 处理)
- 示例:最佳实践:多条
1
2RUN apt-get update && apt-get install -y nginx # 安装 Nginx
RUN ["pip", "install", "flask"] # 安装 Python 包RUN
指令会创建多个层。为了减少镜像层数和镜像大小,通常会将相关的RUN
指令通过&&
连接起来,并在最后清理不必要的文件(如apt-get clean
或删除缓存)。
3. COPY
- 作用:将构建上下文(通常是 Dockerfile 所在的目录)中的文件或目录拷贝到镜像中。
- 语法:
COPY <src>... <dest>
- 示例:
1
2COPY . /app # 将当前目录所有内容拷贝到镜像的 /app 目录
COPY requirements.txt /tmp/ # 拷贝单个文件
4. ADD
- 作用:与
COPY
类似,但ADD
具有额外功能:- 如果
<src>
是一个 URL,它会下载文件。 - 如果
<src>
是一个压缩包(如.tar
,.gz
),它会自动解压到<dest>
。
- 如果
- 语法:
ADD <src>... <dest>
- 最佳实践:通常建议优先使用
COPY
,因为它的行为更可预测。只有在需要自动解压或从 URL 下载时才使用ADD
。
5. WORKDIR
- 作用:设置
RUN
,CMD
,ENTRYPOINT
和COPY
/ADD
指令的工作目录。如果目录不存在,WORKDIR
会自动创建它。 - 语法:
WORKDIR /path/to/workdir
- 示例:最佳实践:在
1
2WORKDIR /app # 将 /app 设为后续指令的工作目录
COPY . . # 现在表示拷贝到 /app 目录下COPY
/ADD
之前设置WORKDIR
可以简化路径。
6. EXPOSE
- 作用:声明容器将监听哪些端口。这仅仅是文档性质的声明,并不会实际发布端口。真正的端口映射需要在
docker run
命令中使用-p
或docker-compose.yml
中配置。 - 语法:
EXPOSE <port> [<port>...]
- 示例:
1
2EXPOSE 80 # 声明容器监听 80 端口
EXPOSE 80 443 # 声明监听 80 和 443 端口
7. CMD
- 作用:为容器提供默认的执行命令。如果
docker run
命令指定了其他命令,则CMD
将被忽略。一个 Dockerfile 中只能有一条CMD
指令,如果有多条,只有最后一条生效。 - 语法:
CMD ["executable","param1","param2"]
(exec 格式,推荐)CMD command param1 param2
(shell 格式)CMD ["param1","param2"]
(作为ENTRYPOINT
的默认参数)
- 示例:最佳实践:推荐使用 exec 格式,因为它更清晰,且避免了 shell 的额外开销。
1
2CMD ["nginx", "-g", "daemon off;"] # 以 exec 格式启动 Nginx
CMD python3 app.py # 以 shell 格式启动 Python 应用
8. ENTRYPOINT
- 作用:提供容器启动时要执行的固定命令或脚本。它不会被
docker run
命令中的参数覆盖,而是将docker run
命令的参数作为其自身的参数。一个 Dockerfile 中只能有一条ENTRYPOINT
指令。 - 语法:
ENTRYPOINT ["executable", "param1", "param2"]
(exec 格式,推荐) - 示例:在这种组合下,
1
2ENTRYPOINT ["/usr/bin/supervisord"] # 总是以 supervisord 启动
CMD ["-c", "/etc/supervisor/conf.d/supervisord.conf"] # CMD 作为 ENTRYPOINT 的默认参数docker run my_image
会执行/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
。如果docker run my_image ls -l
,则会执行/usr/bin/supervisord ls -l
。
CMD
和 ENTRYPOINT
的区别总结:
特性 | CMD | ENTRYPOINT |
---|---|---|
覆盖 | 可被 docker run 后面的命令覆盖 | 不会被 docker run 后面的命令覆盖,而是将其作为参数 |
用途 | 提供默认命令或 ENTRYPOINT 的默认参数 | 定义容器的主入口点(可执行文件/脚本) |
数量 | 一个 Dockerfile 中只能有一条 | 一个 Dockerfile 中只能有一条 |
9. ENV
- 作用:设置环境变量。这些变量在镜像构建时和容器运行时都可用。
- 语法:
ENV <key>=<value> ...
- 示例:
1
2ENV APP_PORT=5000
ENV DATABASE_URL=mongodb://localhost:27017/myapp
10. ARG
- 作用:定义构建时变量。这些变量可以在
docker build
命令中通过--build-arg
参数传递。它们只在构建过程中有效,容器运行时不会保留。 - 语法:
ARG <name>[=<default value>]
- 示例:
1
2ARG NODE_VERSION=16
FROM node:${NODE_VERSION}-alpine
11. VOLUME
- 作用:声明一个挂载点,用于将数据从容器文件系统分离,实现数据持久化或共享。
- 语法:
VOLUME ["/data"]
或VOLUME /data
- 示例:这仅是声明一个意图,实际的卷挂载需要在
1
VOLUME /var/lib/mysql # 声明 /var/lib/mysql 目录为数据卷
docker run -v
或docker-compose.yml
中指定。
12. USER
- 作用:设置执行后续指令或容器启动时使用的用户或用户组。
- 语法:
USER <user>[:<group>]
- 示例:最佳实践:为了安全,尽量避免使用
1
USER appuser # 后续操作将以 appuser 身份运行
root
用户运行容器进程。
构建一个简单的 Flask 应用 Dockerfile 示例
现在,我们来构建一个包含 Python Flask 应用的 Docker 镜像。
首先,准备以下文件:
app.py
(Flask 应用代码):
1 | from flask import Flask |
requirements.txt
(Python 依赖):
Flask==2.0.3
Dockerfile
:
1 | # 1. 指定基础镜像:我们选择一个轻量级的 Python 镜像 |
构建镜像:
在包含 app.py
, requirements.txt
和 Dockerfile
的目录下,打开终端并执行:
1 | docker build -t my-flask-app:1.0 . |
-t my-flask-app:1.0
为你的镜像指定了名称和标签,.
表示 Dockerfile 位于当前目录。
运行容器:
1 | docker run -d -p 5000:5000 --name flask-web-app my-flask-app:1.0 |
运行后,您可以在浏览器中访问 http://localhost:5000
,看到 Hello, Docker World!
。
Dockerfile 构建缓存机制
Docker 在构建镜像时会逐行执行 Dockerfile 中的指令。每条指令的执行结果都会被缓存为一个新的镜像层。如果某条指令及其上下文没有发生变化,Docker 会直接使用缓存层,从而大大加快构建速度。
利用缓存的策略:
- 将不经常变化的指令放在 Dockerfile 的前面:例如
FROM
、COPY requirements.txt
、RUN pip install
。 - 将经常变化的指令放在 Dockerfile 的后面:例如
COPY . .
(应用程序代码)。 - 避免在
RUN
指令中进行不必要的变更:例如,将apt-get update
和apt-get install
写在一起,避免apt-get update
单独一层导致后续缓存失效。
结语
本篇文章深度解析了 Dockerfile 的常用指令,并通过一个实际的 Flask 应用构建示例,让您了解了如何编写 Dockerfile 并构建自己的镜像。掌握 Dockerfile 是将应用程序容器化的核心技能。
在下一篇文章中,我们将学习如何管理容器的数据。虽然容器本身是临时的,但应用程序的数据往往需要持久化存储。我们将深入探讨 Docker Volume (数据卷) 的概念和用法。