Docker05-Dockerfile 与镜像构建
Dockerfile 与镜像构建:从代码到镜像
目录
- 什么是 Dockerfile
- Dockerfile 常用指令详解
- 一个完整的构建示例
- 构建镜像:docker build
- 层缓存机制:理解镜像构建的加速原理
- 多阶段构建:给镜像瘦身
- CMD 与 ENTRYPOINT 的区别
- .dockerignore:排除无关文件
- 健康检查:HEALTHCHECK
- 构建的常见问题与优化
- 总结
- 参考资料
0. 什么是 Dockerfile
Dockerfile 是一个文本文件,用一系列指令描述「如何从一个基础镜像构建出新镜像」。它定义了:
- 用什么基础镜像;
- 安装哪些软件、复制哪些文件;
- 设置什么环境变量、暴露哪些端口;
- 容器启动时运行什么命令。
Dockerfile 让镜像构建可复现、可版本化、可审查——任何人拿到 Dockerfile 都能构建出行为一致的镜像。这正是「基础设施即代码」思想的体现。
初级 Podman 篇里你见过 Containerfile/Dockerfile 的简单用法。这一篇系统讲透所有常用指令,以及构建的高级技巧。
一、Dockerfile 常用指令详解
1.1 FROM:指定基础镜像
每个 Dockerfile 必须以 FROM 开头,指定基础镜像:
FROM ubuntu:24.04 # 用官方 Ubuntu 作为基础
FROM python:3.12-slim # 带 Python 运行时的基础镜像
FROM alpine:3.20 # 精简版基础镜像(体积小)
基础镜像决定了你最终镜像的底层。官方镜像(带 :版本 的)一般都有优化,优先选择带 -slim 或 -alpine 的精简版。
1.2 RUN:在构建时执行命令
RUN 在构建阶段执行命令(安装软件、编译、建目录等):
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl nginx
每个 RUN 都会生成一层。尽量把多条命令用 && 连成一条 RUN,可以减少层数、避免产生中间垃圾。
1.3 COPY 与 ADD:复制文件
- COPY:把构建上下文(项目目录)里的文件复制进镜像;
- ADD:类似 COPY,但支持从 URL 下载、自动解压 tar 包(功能更多,但行为不够透明,通常优先用 COPY)。
COPY . /app # 把当前目录复制到镜像 /app
COPY app.py /app/app.py # 复制单个文件
1.4 WORKDIR:设置工作目录
WORKDIR 设置后续指令(RUN、CMD 等)的工作目录,不存在会自动创建:
WORKDIR /app
RUN pwd # 会输出 /app
1.5 ENV:设置环境变量
ENV APP_ENV=production \
PORT=8080
镜像运行时,容器里也会有这些环境变量。
1.6 EXPOSE:声明端口
EXPOSE 只是声明容器会监听哪些端口(文档作用),并不真正映射端口。真正映射靠运行时的 -p:
EXPOSE 80
1.7 USER:指定运行用户
出于安全,尽量不用 root 运行应用:
USER appuser # 后续以 appuser 身份运行
1.8 CMD 与 ENTRYPOINT:指定启动命令
- CMD:容器启动时执行的命令,可被
docker run后面的参数覆盖; - ENTRYPOINT:容器启动时执行的主命令,不易被覆盖(常与 CMD 配合给默认参数)。
CMD ["nginx", "-g", "daemon off;"]
详见第 6 章。
二、一个完整的构建示例
下面构建一个简单的静态网站镜像(nginx)。
2.1 准备项目文件
在项目目录里准备一个 Dockerfile 和一个网页:
myapp/
├── Dockerfile
└── index.html
index.html:
<!DOCTYPE html>
<html>
<head><title>Hello Docker</title></head>
<body><h1>Hello from Docker!</h1></body>
</html>
2.2 编写 Dockerfile
# 基于 nginx 官方镜像
FROM nginx:1.25
# 把本地 index.html 复制到 nginx 的默认网站目录
COPY index.html /usr/share/nginx/html/index.html
# 声明容器监听 80 端口
EXPOSE 80
2.3 构建并运行
# 在 myapp 目录下构建,-t 指定镜像名和标签,最后的 . 是构建上下文
docker build -t myweb:v1 .
# 运行
docker run -d --name web -p 8080:80 myweb:v1
访问 http://本机IP:8080 就能看到「Hello from Docker!」。
最后的
.表示**构建上下文(build context)**是当前目录。Docker 会把当前目录(受 .dockerignore 约束)发送给构建进程。
三、构建镜像:docker build
3.1 基本命令
# 用当前目录的 Dockerfile 构建
docker build -t myapp:v1 .
# 指定 Dockerfile 路径(-f)
docker build -f path/to/Dockerfile -t myapp:v1 .
# 指定其他构建上下文目录
docker build -t myapp:v1 /some/project
-t:给镜像命名(可多次指定多个名字);.:构建上下文目录;- 默认会在上下文目录里找名为
Dockerfile的文件。
3.2 构建过程输出
构建时 Docker 会逐条执行指令,每条指令生成一层,输出会显示每一步(Step 1/2/3...)。构建出错会停在那一步并给出提示。
3.3 查看与运行构建的镜像
docker images # 看到 myapp:v1
docker run -d --name app -p 8080:80 myapp:v1
四、层缓存机制:理解镜像构建的加速原理
4.1 缓存是怎么工作的
Docker 构建时逐层缓存:如果某条指令的输入(基础镜像、上一步结果、复制的内容)没变,Docker 会复用缓存层,跳过执行,从而大幅加快重复构建。
4.2 利用缓存的顺序原则
指令顺序影响缓存命中率。把「不容易变」的放前面,把「容易变」的放后面:
FROM python:3.12-slim
# 先装依赖(requirements.txt 很少变)——优先缓存
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt
# 后复制代码(代码经常变)
COPY . /app
如果先 COPY . /app 再装依赖,那么每次改代码都会让后面的依赖安装层缓存失效,构建变慢。
4.3 缓存失效
- 某条指令的输入变化,该指令及之后所有层缓存失效;
- 可以用
--no-cache强制不缓存(docker build --no-cache -t myapp:v1 .),用于排障或保证完全从零构建。
五、多阶段构建:给镜像瘦身
5.1 问题:构建环境太胖
编译型应用(如 Go、Java、C)需要完整的编译工具链,但这些工具运行时用不到。如果用一个包含全部工具链的镜像当最终镜像,会非常臃肿且不安全。
5.2 多阶段构建:一个 Dockerfile 分多个 FROM
多阶段构建在同一个 Dockerfile 里用多个 FROM,前面的阶段负责编译,最后只把编译产物复制到精简的最终阶段:
# 阶段1:编译环境
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# 阶段2:精简运行环境
FROM alpine:3.20
WORKDIR /app
# 只把编译好的二进制复制过来
COPY --from=builder /app/myapp .
CMD ["./myapp"]
AS builder 给第一阶段命名,COPY --from=builder 从该阶段取文件。最终镜像只包含第二阶段的内容(体积小很多)。
5.3 多阶段构建的好处
- 体积小:运行时不需要编译工具链;
- 更安全:减少攻击面(没有多余工具);
- 单一文件:一个 Dockerfile 管理构建全过程。
六、CMD 与 ENTRYPOINT 的区别
6.1 CMD:可被覆盖的默认命令
CMD 定义容器启动时的默认命令,但运行时可被覆盖:
CMD ["nginx", "-g", "daemon off;"]
# 运行时用其他命令覆盖 CMD
docker run myimage echo "hi"
6.2 ENTRYPOINT:主命令,不可轻易覆盖
ENTRYPOINT 定义容器启动时的主命令,不易被 docker run 参数覆盖:
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"] # 作为默认参数传给 ENTRYPOINT
运行时:docker run myimage --port 9090,后面的参数会替换 CMD 作为 ENTRYPOINT 的入参。
6.3 什么时候用哪个
- 有固定主程序的镜像:用 ENTRYPOINT(如服务进程);
- 允许用户传参/换命令的工具镜像:用 CMD;
- 两者常配合:ENTRYPOINT 定主程序,CMD 给默认参数。
注意:推荐用 exec 形式(
["cmd", "arg"]数组写法),不要用 shell 形式(CMD nginx -g ...),前者能正确处理信号、进程更干净。
七、.dockerignore:排除无关文件
7.1 为什么需要
构建时 Docker 会把整个构建上下文发给守护进程。如果目录里有 node_modules、.git、日志、临时文件等,会被一起打包,拖慢构建、增大镜像、甚至泄露敏感文件。
7.2 使用 .dockerignore
在项目根目录创建 .dockerignore,语法类似 .gitignore:
.git
node_modules
*.log
.DS_Store
build/
这样这些文件就不会被纳入构建上下文。
八、健康检查:HEALTHCHECK
8.1 什么是健康检查
HEALTHCHECK 让 Docker 能周期性检查容器是否「健康」,供编排系统(Compose、Swarm、K8s)判断容器是否可用:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
--interval:检查间隔;--timeout:单次超时;--start-period:启动宽限(容器刚起不算失败);--retries:连续失败多少次判定不健康。
8.2 查看健康状态
docker ps # 看到 STATUS 里的 (healthy)
docker inspect -f '{{.State.Health.Status}}' <容器>
生产环境给服务加上健康检查,是编排系统能做滚动更新、自动重启的前提。
九、构建的常见问题与优化
9.1 构建报错找不到包/源
- 基础镜像内可能没有更新源,先
RUN apt-get update(或对应包管理器 update); - 国内构建时,在 RUN 里临时换用国内源(如 apt 换阿里云、pip 换清华)加速。
9.2 镜像太大
- 用精简基础镜像(-slim / -alpine);
- 用多阶段构建;
- 合并 RUN 减少层数;
- 清理安装后的缓存(
&& rm -rf /var/lib/apt/lists/*)。
9.3 COPY 的文件没生效
确认 .dockerignore 没把目标文件排除,确认构建上下文路径正确。
9.4 缓存导致构建结果「不对」
怀疑缓存问题时,用 --no-cache 强制重新构建排查。
9.5 构建太慢
- 善用层缓存(把依赖安装放前面);
- 精简构建上下文(.dockerignore);
- 使用国内镜像加速。
十、总结
- Dockerfile 用指令描述镜像构建,实现可复现、可版本化;
- 常用指令:FROM、RUN、COPY、ADD、WORKDIR、ENV、EXPOSE、USER、CMD、ENTRYPOINT;
- 构建:
docker build -t 名字 .,.是构建上下文; - 层缓存:把不变的放前面、易变的放后面,能大幅加速重复构建;
- 多阶段构建:编译阶段 + 精简运行阶段,显著减小镜像;
- CMD vs ENTRYPOINT:CMD 可被覆盖,ENTRYPOINT 定主命令,推荐 exec 形式;
- .dockerignore 排除无关文件;HEALTHCHECK 提供健康检查。
参考资料
- Docker 官方:Dockerfile 参考:https://docs.docker.com/engine/reference/builder/
- Docker 官方:多阶段构建:https://docs.docker.com/build/building/multi-stage/
- Docker 官方:最佳实践(Best practices):https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Docker 官方:.dockerignore:https://docs.docker.com/build/building/context/#dockerignore-files
