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