在容器化技术中,Dockerfile 是构建 Docker 镜像的灵魂。
它是一个纯文本脚本,包含了按顺序排列的指令,定义了如何从一个基础操作系统开始,逐步叠加环境依赖、配置参数和应用程序,最终打包成一个可移植、自包含的执行单元——镜像 (Image)。
一、 Dockerfile 示例
编写 Dockerfile 时,每一行都代表一个构建步骤。以下是一个生产环境通用的后端应用构建示例:
dockerfile
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY target/my-server.jar app.jar
ENV TZ=Asia/Shanghai
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]关键指令说明:
- FROM:必须是第一条指令,定义了镜像的“底座”。
- WORKDIR:类似于 Linux 的
cd,为后续指令提供上下文路径。 - COPY vs ADD:
COPY仅负责单纯的文件复制;ADD则功能更强,支持解压.tar.gz文件或从 URL 下载文件。通常推荐使用更透明的COPY。 - ENTRYPOINT:定义容器启动时运行的第一个程序。相比
CMD,它更难被启动命令覆盖,适合作为服务的固定启动脚本。
二、 核心原理:联合文件系统 (UnionFS)
Dockerfile 构建镜像的本质是分层 (Layering)。
- 只读层:Dockerfile 中的每一条指令(如
RUN、COPY、ADD)都会创建一个新的镜像层。镜像就像是一个“千层饼”,每一层都是相对于上一层的文件增量。 - 写时复制 (CoW):当容器启动时,Docker 会在镜像的最顶层添加一个薄薄的可写层 (Container Layer)。所有的修改、日志写入都发生在这里,而底部的镜像层永远是只读且不可变的。
- 分层的好处:
- 资源共享:如果多个镜像都基于同一个
openjdk:17,它们在物理磁盘上会共享这一层,极大节省空间。 - 构建缓存:如果代码没变,只有配置变了,再次构建时 Docker 会直接复用之前的
COPY层缓存,秒级完成构建。
- 资源共享:如果多个镜像都基于同一个
三、 关于端口声明 (EXPOSE) 的深度解析
问题:EXPOSE 声明了端口,后续 build 或 run 时可以指定不一样的端口吗?
答案是:可以。 这是一个常见的认知误区,需要明确以下几点:
- EXPOSE 只是声明,不是强制:
EXPOSE 8080并不等同于防火墙开通,它更像是一种“元数据”或文档说明,告知运维人员和 Docker 系统:这个镜像里的程序默认在 8080 端口监听。 - 运行时映射 (Publish):真正决定外部访问端口的是在
docker run阶段。- 如果你执行
docker run -p 8081:8081 my-server,那么外部访问服务器的 8081 端口,流量会被转发到容器内的 8081 端口。 - 结论:你可以将容器内部声明的任何端口映射到宿主机的任意可用端口上。
- 如果你执行
- Build 时不可更改:端口声明是写死在镜像元数据里的,
docker build阶段无法动态修改EXPOSE。但由于它只是一个声明,你完全可以在运行时忽略它,按需映射。
四、 Dockerfile 构建常用命令
编写完文件后,通过以下命令将脚本转化为物理镜像:
- 构建镜像:
docker build -t my-server:v1.0 .(末尾的点.非常重要,它代表构建上下文目录,Docker 会在该目录下寻找 Dockerfile 并读取文件)。 - 查看构建出的镜像:
docker images - 运行镜像测试:
docker run -d --name my-running-app -p 8080:8080 my-server:v1.0
五、 最佳实践总结
- 镜像瘦身:尽量选择
alpine等体积小的基础镜像。 - 减少层数:通过
&&合并多条RUN命令,可以减少镜像层数,降低存储开销。 - 敏感信息:永远不要在 Dockerfile 中写死数据库密码等敏感信息,应通过
ENV在运行时注入。 - 顺序优化:将不经常变动的步骤(安装基础库)放在前面,经常变动的步骤(复制源码)放在后面,以充分利用构建缓存。