Docker 的核心使用方式可以简单理解为:
Dockerfile
↓
docker build
↓
Docker Image
↓
docker run
↓
Docker Container
如果说 Docker Container 解决的是“应用怎么运行”,那么 Dockerfile 解决的就是“应用运行环境怎么构建”。
在实际项目中,我们很少直接使用一个现成镜像完成所有工作。对于 PHP、Java、Node.js、Go 等应用,通常需要把项目代码、运行环境、依赖以及启动方式封装成自己的镜像。
Docker 官方将 Dockerfile 定义为描述镜像构建过程的文本文件,Docker 会按照其中的指令顺序构建镜像。Dockerfile 通常从 FROM 开始,并支持 RUN、COPY、CMD、ENTRYPOINT、ENV、ARG、WORKDIR、USER、HEALTHCHECK 等指令。
1. Dockerfile 是什么
一个最简单的 Dockerfile:
FROM nginx:1.27
COPY ./html /usr/share/nginx/html
然后执行:
docker build -t my-nginx:1.0 .
Docker 会读取当前目录下的 Dockerfile,并根据指令构建镜像。
这里有两个重要概念:
Dockerfile
定义:
使用什么基础镜像
安装什么软件
复制什么文件
设置什么环境变量
使用什么用户
容器启动时运行什么程序
Image
Dockerfile 执行完成后得到镜像。
例如:
Dockerfile
│
│ docker build
▼
my-nginx:1.0
然后:
docker run -d --name nginx my-nginx:1.0
才能真正创建并运行容器。
2. Dockerfile 基本结构
Dockerfile 的基本格式非常简单:
INSTRUCTION arguments
例如:
FROM php:8.3-cli
WORKDIR /app
COPY . /app
RUN php -v
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public"]
Docker 按照从上到下的顺序执行这些指令。FROM 用于初始化构建阶段,后续指令都建立在这个阶段之上。一个 Dockerfile 可以存在多个 FROM,从而实现多阶段构建。
因此可以把 Dockerfile 理解成:
基础镜像
↓
工作目录
↓
安装依赖
↓
复制代码
↓
设置环境
↓
指定启动方式
↓
生成最终镜像
3. Dockerfile 核心指令
Dockerfile 指令很多,但日常开发最重要的是:
FROM
RUN
WORKDIR
COPY
ADD
ENV
ARG
EXPOSE
USER
CMD
ENTRYPOINT
HEALTHCHECK
3.1 FROM
FROM 指定基础镜像。
FROM php:8.3-cli
也可以:
FROM nginx:1.27
或者:
FROM alpine:3.20
基础镜像直接影响最终镜像的:
操作系统环境
已安装的软件
CPU 架构兼容性
镜像大小
安全更新策略
生产环境不建议无条件使用:
FROM php:latest
更推荐明确版本:
FROM php:8.3-cli
如果对构建可重复性要求更高,还可以进一步固定镜像 Digest。
3.2 RUN
RUN 用于构建镜像时执行命令。
例如:
RUN apt-get update
RUN apt-get install -y git unzip
也可以合并:
RUN apt-get update \
&& apt-get install -y git unzip \
&& rm -rf /var/lib/apt/lists/*
RUN 与 CMD 是非常容易混淆的一对指令:
RUN composer install
表示:
构建镜像时执行 Composer。
而:
CMD ["php", "server.php"]
表示:
容器启动后默认执行 PHP。
Docker 官方文档也明确区分了两者:RUN 在构建阶段执行并产生镜像层,而 CMD 不在构建阶段执行,它定义的是镜像的默认启动命令。
3.3 WORKDIR
设置工作目录:
WORKDIR /app
后面的:
RUN
COPY
CMD
ENTRYPOINT
等指令都可以基于这个目录工作。
例如:
WORKDIR /app
COPY . .
RUN composer install
相当于:
/app
├── composer.json
├── composer.lock
├── app
├── config
└── ...
相比:
RUN cd /app
更推荐使用 WORKDIR。
3.4 COPY
把构建上下文中的文件复制到镜像:
COPY . /app
也可以只复制指定文件:
COPY composer.json composer.lock /app/
或者:
COPY ./config /app/config
实际项目中推荐尽量明确复制内容,而不是无脑:
COPY . .
因为 Docker 构建上下文中可能包含:
.git
node_modules
vendor
.env
日志
临时文件
这些内容通常没有必要进入镜像。
因此一般还需要配合:
.dockerignore
使用 .dockerignore 可以排除不需要发送到构建上下文中的文件。
例如:
.git
.gitignore
.env
vendor
node_modules
runtime
storage
*.log
3.5 ADD
ADD 也可以复制文件:
ADD . /app
但 ADD 具有一些额外能力,例如支持远程 URL、Git 仓库以及某些归档处理。
对于普通项目文件复制,通常优先使用:
COPY
而不是:
ADD
这样 Dockerfile 的意图更加明确。
3.6 ENV
设置镜像运行时环境变量:
ENV APP_ENV=production
容器启动后可以:
docker exec app env
查看。
例如:
ENV APP_ENV=production
ENV TZ=Asia/Shanghai
需要注意:
不要使用 ENV 保存密码、Token、数据库密钥等敏感信息。
镜像本身需要被多人、多个环境复用时,这些信息应该通过运行时环境变量、Secrets 等机制注入。
3.7 ARG
ARG 是构建阶段参数:
ARG PHP_VERSION=8.3
FROM php:${PHP_VERSION}-cli
构建时:
docker build \
--build-arg PHP_VERSION=8.4 \
-t my-app:1.0 .
ARG 和 ENV 的主要区别:
| 指令 | 主要作用 | 生命周期 |
|---|---|---|
ARG | 构建参数 | 构建阶段 |
ENV | 环境变量 | 镜像/容器运行阶段 |
尤其要注意:
不要使用 ARG 传递密码、Token 等 Secret。
Docker 官方明确提示,构建参数可能通过 docker history 等方式暴露,因此不适合作为秘密信息传递机制。
3.8 EXPOSE
声明应用监听的端口:
EXPOSE 9501
例如 Hyperf:
EXPOSE 9501
需要注意:
EXPOSE 本身不会把宿主机端口映射到容器。
真正进行端口映射的是:
docker run -p 9501:9501 ...
或者 Compose:
ports:
- "9501:9501"
因此:
EXPOSE
是镜像元数据/声明。
而:
-p
ports
才是真正的端口发布。
3.9 CMD
定义容器默认启动命令:
CMD ["php", "bin/hyperf.php", "start"]
也可以:
CMD ["nginx", "-g", "daemon off;"]
推荐使用 Exec Form:
CMD ["php", "server.php"]
而不是:
CMD php server.php
Exec Form 对信号处理和进程管理通常更加明确。
3.10 ENTRYPOINT
ENTRYPOINT 用于定义容器的主要执行程序:
ENTRYPOINT ["php"]
然后:
CMD ["server.php"]
组合后相当于:
php server.php
常见理解方式:
ENTRYPOINT = 容器主要程序
CMD = 默认参数
例如:
ENTRYPOINT ["php"]
CMD ["-v"]
运行:
docker run my-php
相当于:
php -v
而:
docker run my-php -m
则可以覆盖 CMD。
4. Docker 构建上下文与镜像缓存
执行:
docker build -t my-app:1.0 .
最后的:
.
不是简单的“Dockerfile 所在目录”。
它表示:
Build Context,构建上下文。
Docker 在构建过程中可以访问这个上下文中的文件。
例如:
project/
├── Dockerfile
├── composer.json
├── composer.lock
├── app/
├── config/
└── .dockerignore
执行:
docker build -t my-app:1.0 .
表示把当前目录作为构建上下文。
4.1 为什么 Dockerfile 要考虑指令顺序
Docker 构建会利用缓存。
例如:
FROM php:8.3-cli
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install
COPY . .
CMD ["php", "server.php"]
这种结构通常比:
FROM php:8.3-cli
WORKDIR /app
COPY . .
RUN composer install
更合理。
原因是:
composer.json
composer.lock
↓
composer install
只有依赖文件发生变化时才需要重新安装依赖。
而业务代码:
COPY .
变化非常频繁。
因此应该尽可能把变化频率低的内容放前面,变化频率高的内容放后面。
5. PHP 应用 Dockerfile 实战
以一个 PHP 项目为例:
project/
├── Dockerfile
├── .dockerignore
├── composer.json
├── composer.lock
├── bin/
├── config/
├── app/
└── public/
Dockerfile:
FROM php:8.3-cli
WORKDIR /app
COPY composer.json composer.lock ./
RUN apt-get update \
&& apt-get install -y \
git \
unzip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--optimize-autoloader
COPY . .
EXPOSE 9501
CMD ["php", "bin/hyperf.php", "start"]
构建:
docker build -t my-hyperf-app:1.0 .
运行:
docker run -d \
--name hyperf \
-p 9501:9501 \
my-hyperf-app:1.0
查看:
docker logs -f hyperf
这样就形成了完整链路:
PHP 项目
↓
Dockerfile
↓
docker build
↓
my-hyperf-app:1.0
↓
docker run
↓
Hyperf Container
6. 多阶段构建
实际项目中,经常会遇到一个问题:
构建环境需要很多工具,但运行环境并不需要这些工具。
例如:
Composer
Git
Node.js
编译器
开发依赖
这些东西可能只是为了构建应用。
最终运行容器实际上只需要:
PHP Runtime
应用代码
生产依赖
这时可以使用 Multi-stage Build。
例如:
FROM composer:2 AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--optimize-autoloader
COPY . .
FROM php:8.3-cli
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 9501
CMD ["php", "bin/hyperf.php", "start"]
这里:
FROM composer:2 AS builder
是构建阶段。
而:
FROM php:8.3-cli
是最终运行阶段。
最终镜像只复制:
COPY --from=builder /app /app
所需要的内容。
因此:
Builder Image
│
├── Composer
├── Git
├── Build Tools
└── Dependencies
│
▼
Final Image
│
├── PHP
├── Application
└── Production Dependencies
这是生产环境中非常常见的镜像构建方式。
7. Dockerfile 常见问题
7.1 为什么代码修改后每次都重新安装 Composer?
通常是因为:
COPY . .
RUN composer install
代码任何变化都会导致 COPY . . 缓存失效,后面的 composer install 也需要重新执行。
更合理:
COPY composer.json composer.lock ./
RUN composer install
COPY . .
7.2 为什么镜像特别大?
常见原因:
安装了不必要的软件
没有清理 apt 缓存
把 node_modules 复制进去了
把 .git 复制进去了
把 vendor 构建环境全部保留
没有使用多阶段构建
首先检查:
docker images
然后:
docker history my-app:1.0
查看每一层到底增加了什么。
7.3 为什么容器启动后马上退出?
首先:
docker ps -a
然后:
docker logs my-app
重点检查:
CMD
ENTRYPOINT
因为容器生命周期通常与其主进程生命周期直接相关:
主进程启动
↓
正常运行
↓
主进程退出
↓
容器结束
8. Dockerfile 编写规范
实际项目中建议遵循以下原则。
1. 固定基础镜像版本
避免:
FROM php:latest
优先:
FROM php:8.3-cli
2. 合理利用缓存
把变化频率低的内容放在前面:
COPY composer.json composer.lock ./
RUN composer install
COPY . .
3. 使用 .dockerignore
避免把:
.git
.env
node_modules
vendor
日志
临时文件
复制进构建上下文。
4. 使用多阶段构建
将:
构建环境
与:
运行环境
分离。
5. 不要把 Secret 写进 Dockerfile
避免:
ENV DB_PASSWORD=123456
也避免:
ARG API_KEY=xxxxx
6. 明确容器启动程序
例如:
CMD ["php", "bin/hyperf.php", "start"]
不要依赖复杂的 Shell 脚本隐藏真正的启动逻辑。
7. 尽可能使用非 Root 用户
如果应用允许,可以使用:
USER www-data
降低容器进程权限。
9. Dockerfile 常用命令
构建:
docker build -t my-app:1.0 .
指定 Dockerfile:
docker build -f Dockerfile.prod -t my-app:prod .
指定构建参数:
docker build \
--build-arg PHP_VERSION=8.4 \
-t my-app:1.0 .
查看镜像:
docker images
查看镜像构建历史:
docker history my-app:1.0
查看镜像详细信息:
docker inspect my-app:1.0
运行:
docker run -d --name my-app my-app:1.0
10. 总结
Dockerfile 的核心其实可以浓缩成一条主线:
FROM
↓
WORKDIR
↓
RUN
↓
COPY
↓
ENV / ARG
↓
EXPOSE
↓
CMD / ENTRYPOINT
↓
docker build
↓
Image
真正编写生产级 Dockerfile 时,重点并不是记住所有指令,而是理解三个问题:
第一,镜像里面到底应该有什么?
只保留运行应用真正需要的内容。
第二,哪些内容应该缓存?
把稳定依赖放在前面,把经常变化的代码放在后面。
第三,构建环境和运行环境是否应该分离?
如果构建工具很多,就应该考虑多阶段构建。
掌握 Dockerfile 后,下一步就是使用 Docker Compose 把这个镜像与 MySQL、Redis、Nginx 等其他容器组织起来。
Dockerfile 官方参考文档:Dockerfile Reference