Skip to content
Dockerfile 核心原理与构建指南

在容器化技术中,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 ADDCOPY 仅负责单纯的文件复制;ADD 则功能更强,支持解压 .tar.gz 文件或从 URL 下载文件。通常推荐使用更透明的 COPY
  • ENTRYPOINT:定义容器启动时运行的第一个程序。相比 CMD,它更难被启动命令覆盖,适合作为服务的固定启动脚本。

二、 核心原理:联合文件系统 (UnionFS)

Dockerfile 构建镜像的本质是分层 (Layering)

  1. 只读层:Dockerfile 中的每一条指令(如 RUNCOPYADD)都会创建一个新的镜像层。镜像就像是一个“千层饼”,每一层都是相对于上一层的文件增量。
  2. 写时复制 (CoW):当容器启动时,Docker 会在镜像的最顶层添加一个薄薄的可写层 (Container Layer)。所有的修改、日志写入都发生在这里,而底部的镜像层永远是只读且不可变的。
  3. 分层的好处
    • 资源共享:如果多个镜像都基于同一个 openjdk:17,它们在物理磁盘上会共享这一层,极大节省空间。
    • 构建缓存:如果代码没变,只有配置变了,再次构建时 Docker 会直接复用之前的 COPY 层缓存,秒级完成构建。

三、 关于端口声明 (EXPOSE) 的深度解析

问题:EXPOSE 声明了端口,后续 build 或 run 时可以指定不一样的端口吗?

答案是:可以。 这是一个常见的认知误区,需要明确以下几点:

  1. EXPOSE 只是声明,不是强制EXPOSE 8080 并不等同于防火墙开通,它更像是一种“元数据”或文档说明,告知运维人员和 Docker 系统:这个镜像里的程序默认在 8080 端口监听。
  2. 运行时映射 (Publish):真正决定外部访问端口的是在 docker run 阶段。
    • 如果你执行 docker run -p 8081:8081 my-server,那么外部访问服务器的 8081 端口,流量会被转发到容器内的 8081 端口。
    • 结论:你可以将容器内部声明的任何端口映射到宿主机的任意可用端口上。
  3. 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

五、 最佳实践总结

  1. 镜像瘦身:尽量选择 alpine 等体积小的基础镜像。
  2. 减少层数:通过 && 合并多条 RUN 命令,可以减少镜像层数,降低存储开销。
  3. 敏感信息:永远不要在 Dockerfile 中写死数据库密码等敏感信息,应通过 ENV 在运行时注入。
  4. 顺序优化:将不经常变动的步骤(安装基础库)放在前面,经常变动的步骤(复制源码)放在后面,以充分利用构建缓存。