为什么要优化 Docker 镜像
Docker 镜像并不是一个简单的压缩包,而是由多个只读 Layer 组成的文件系统。
例如:
Dockerfile
│
├── FROM
├── RUN
├── COPY
└── RUN
│
▼
Image Layers
│
▼
Container
镜像体积会直接影响:
构建时间。
镜像 Push 时间。
镜像 Pull 时间。
CI/CD 执行时间。
磁盘占用。
容器启动速度。
镜像攻击面。
因此,镜像优化并不只是为了“让镜像变小”。
更准确地说:
镜像优化
│
├── 减少无用文件
├── 减少无用依赖
├── 提高构建缓存命中率
├── 缩短构建时间
├── 减少网络传输
└── 降低攻击面
Docker 官方也将“小而聚焦”的镜像作为生产镜像构建的重要目标,并强调镜像大小和安全性之间存在直接关系。
因此,一份生产级 Dockerfile 通常应该同时考虑:
体积
性能
缓存
安全
可维护性
而不是只关注其中一个指标。
选择合适的基础镜像
基础镜像是整个 Docker 镜像的起点。
例如:
FROM ubuntu:24.04
或者:
FROM debian:bookworm
也可以使用更精简的基础镜像:
FROM alpine:3.22
对于 PHP 应用,还可能使用:
FROM php:8.3-cli
或者:
FROM php:8.3-fpm
基础镜像并不是越小越好。
例如:
完整发行版
↓
Debian
↓
Slim
↓
Alpine
镜像可能逐渐变小,但同时也可能带来:
软件包管理方式不同。
C 库实现差异。
调试工具缺失。
第三方扩展兼容性问题。
构建复杂度增加。
因此不要为了追求几十 MB 的差异,盲目更换基础镜像。
更合理的原则是:
选择能够满足应用运行需求的最小基础镜像。
同时应该固定基础镜像版本:
FROM php:8.3-fpm-bookworm
而不是:
FROM php:latest
这样可以减少基础镜像自动变化带来的不可预测性。
使用多阶段构建减少最终镜像
多阶段构建是 Docker 镜像优化中最重要的方法之一。
传统 Dockerfile 可能把编译环境和运行环境全部放进最终镜像:
最终镜像
├── 编译器
├── Git
├── Composer
├── 开发依赖
├── 源代码
├── 构建工具
└── 运行时
实际上生产环境只需要:
最终镜像
├── 运行时
├── 应用代码
└── 生产依赖
多阶段构建就是将两者拆开:
Builder Stage
├── Composer
├── Git
├── GCC
├── 开发依赖
└── 编译工具
│
│ COPY --from
▼
Runtime Stage
├── PHP Runtime
├── Application
└── Production Dependencies
Docker 官方推荐使用 Multi-stage Build,因为它可以将构建环境和最终运行环境分离,从而减少最终镜像体积和攻击面。
例如 PHP 应用:
FROM composer:2 AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-interaction \
--prefer-dist \
--optimize-autoloader
COPY . .
FROM php:8.3-fpm-bookworm
WORKDIR /var/www
COPY --from=builder /app /var/www
CMD ["php-fpm"]
最终镜像不会包含 Builder 阶段中不需要的构建工具。
对于 Node.js:
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
最终 Nginx 镜像只需要保存构建后的静态文件。
合理利用 Docker 构建缓存
Dockerfile 的每条指令都可能产生 Layer。
例如:
COPY . .
RUN composer install
如果修改任何一个 PHP 文件:
COPY . .
↓
Cache Miss
↓
composer install
↓
重新执行
这会导致大量重复工作。
更合理的方式是:
COPY composer.json composer.lock ./
RUN composer install
COPY . .
这样:
composer.json
composer.lock
│
▼
composer install
│
▼
缓存
源码变化
│
▼
COPY . .
只有依赖文件发生变化时,Composer 依赖层才需要重新构建。
因此 Dockerfile 通常应该遵循:
变化频率低的内容
↓
变化频率高的内容
例如:
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist
COPY app ./app
COPY config ./config
COPY public ./public
而不是一开始就:
COPY . .
Docker 官方构建最佳实践也建议合理利用 Build Cache,并通过拆分构建阶段和共享 Stage 来提高构建效率。
使用 .dockerignore 减少构建上下文
执行:
docker build .
最后的:
.
表示当前目录是 Build Context。
如果项目目录中存在:
.git/
node_modules/
vendor/
runtime/
logs/
.env
.idea/
.vscode/
而这些内容没有被排除,它们都可能进入构建上下文。
因此项目根目录应该配置:
.dockerignore
例如 PHP 项目:
.git
.github
.idea
.vscode
vendor
node_modules
runtime
logs
.env
.env.*
Dockerfile*
docker-compose*.yml
*.log
*.tmp
其中尤其需要注意:
.env
.env.*
因为环境变量文件通常包含:
数据库密码
Redis 密码
API Key
Access Token
第三方服务凭证
不应该把它们复制到 Docker Build Context。
.dockerignore 的核心作用有两个:
减少 Build Context
+
避免无关文件进入镜像构建流程
但是要注意:
.dockerignore不能代替 Secrets 管理。
如果 Secret 已经通过其他方式进入 Dockerfile,仍然可能被写入镜像 Layer。
不要把 Secret 写进镜像
这是 Docker 镜像安全中非常重要的一点。
错误示例:
ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKEN
或者:
ENV API_KEY=xxxxxxxx
甚至:
RUN echo "password=123456" > /app/config.ini
问题在于 Docker Image 的 Layer 是持久化的。
即使后续执行:
RUN rm /app/config.ini
之前 Layer 中的数据仍然可能存在。
因此:
Secret
↓
ARG / ENV / RUN
↓
Image Layer
↓
泄露风险
Docker 官方明确指出,Build Arguments 和环境变量不适合传递构建 Secret,因为敏感信息可能持久存在最终镜像或构建历史中。应该使用 BuildKit 的 Secret Mount 或 SSH Mount。
例如:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=composer_auth \
composer install --no-interaction
构建:
docker build \
--secret id=composer_auth,src=$HOME/.composer/auth.json \
-t my-api:latest \
.
Secret 只在对应的构建步骤中临时提供,不需要写入最终镜像。
如果需要访问私有 Git Repository,也可以使用 SSH Mount:
RUN --mount=type=ssh \
git clone git@github.com:example/private-repository.git
这比把 SSH Key 写进 Dockerfile 或 Build Args 中安全得多。
以非 root 用户运行容器
很多基础镜像默认使用:
root
运行应用。
例如:
FROM php:8.3-fpm
WORKDIR /var/www
COPY . .
CMD ["php-fpm"]
如果应用进程被攻击,攻击者可能直接获得容器内的 root 权限。
更合理的方式是创建专用用户:
FROM php:8.3-cli
RUN groupadd --system app \
&& useradd --system --gid app app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["php", "bin/hyperf.php", "start"]
这里:
USER app
表示容器默认以 app 用户运行。
这可以降低容器进程被攻击后的权限范围。
Docker Scout 的默认策略中也包含 Default non-root user 相关检查,说明非 root 运行已经成为容器镜像安全评估中的重要实践。
需要注意:
容器非 root
并不等于:
宿主机绝对安全
它只是降低容器内进程的权限和潜在影响范围。
如果基础设施允许,还可以进一步考虑 Docker Rootless Mode。Rootless Mode 可以让 Docker daemon 和容器都在非 root 用户环境下运行,从而降低 daemon 和 runtime 漏洞带来的风险。
减少镜像中的无用组件
一个常见问题是:
RUN apt-get update \
&& apt-get install -y \
git \
curl \
vim \
wget \
gcc \
make
这些工具可能只在构建阶段需要。
如果最终运行环境根本不需要:
git
gcc
make
vim
wget
就没有必要全部放进 Runtime Image。
因此:
Builder
├── gcc
├── make
├── git
├── composer
└── 开发工具
Runtime
├── PHP
├── 应用
└── 必要运行库
这是多阶段构建的核心价值。
另外,使用 Debian/Ubuntu 类基础镜像安装软件时,也应注意减少不必要的包,并在同一个 RUN 中完成安装和清理:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libzip-dev \
&& rm -rf /var/lib/apt/lists/*
这样可以避免软件包索引长期保留在镜像 Layer 中。
镜像漏洞扫描
即使 Dockerfile 写得非常规范,镜像仍然可能存在漏洞。
例如:
基础镜像
↓
OS Packages
↓
PHP Extensions
↓
Composer Dependencies
↓
Application
任何一层都可能存在 CVE。
因此:
Build
↓
Scan
↓
Push
比:
Build
↓
Push
更加合理。
Docker Scout 可以分析镜像内容,生成 SBOM,并根据漏洞数据库识别镜像中的已知漏洞。
例如:
docker scout cves my-api:latest
查看镜像漏洞。
也可以:
docker scout cves registry.example.com/backend/api:v1.0.0
针对 Registry 中的镜像进行分析。
还可以查看 SBOM:
docker scout sbom my-api:latest
Docker 官方提供的 docker scout sbom 可以列出镜像中包含的软件包及版本信息。
需要注意:
漏洞扫描不是一次性任务。
今天没有漏洞:
v1.0.0
↓
0 CVE
并不意味着下个月仍然没有漏洞。
因为新的 CVE 会不断发布。
因此生产环境需要:
构建时扫描
+
持续监控
+
定期重建
SBOM 与 Provenance
随着软件供应链安全要求提高,仅仅知道:
镜像有没有漏洞
已经不够。
还需要知道:
镜像里面有什么?
镜像从哪里来?
镜像是怎么构建的?
谁构建的?
使用了哪些依赖?
这就是 SBOM 和 Provenance 的作用。
SBOM
SBOM,即 Software Bill of Materials,可以理解为:
软件物料清单。
例如一个 PHP 镜像:
Image
├── Debian
│ ├── libc
│ ├── openssl
│ └── curl
│
├── PHP
│
├── Composer
│
└── Composer Packages
├── symfony/*
├── psr/*
└── hyperf/*
SBOM 记录这些软件组件及其版本等信息。
Docker 的 BuildKit 支持在构建时生成 SBOM Attestation,并将其附加到镜像元数据中。
Provenance
Provenance 更关注:
这个镜像是怎么构建出来的?
例如:
Source Repository
↓
Commit SHA
↓
GitHub Actions
↓
Docker Build
↓
Image
这样可以建立:
代码
↓
构建过程
↓
镜像
之间的关系。
构建时可以使用:
docker buildx build \
--provenance=true \
--sbom=true \
-t registry.example.com/backend/api:v1.0.0 \
--push \
.
Docker 官方目前将 SBOM 和 Provenance 都作为 Build Attestations 的重要组成部分。
将镜像安全检查加入 CI/CD
前面的 Docker CI/CD 流程可以进一步升级:
Git Push
↓
Test
↓
Build
↓
Vulnerability Scan
↓
SBOM
↓
Provenance
↓
Push
↓
Deploy
例如:
- name: Build and Push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.m