ToolkitX
知识库工具箱

Docker 镜像优化

多阶段构建、镜像瘦身

20min·高级

01. 镜像大小的优化——为什么我的镜像 2GB

大镜像的问题:push/pull 慢、占用磁盘多、启动慢。优化镜像大小是个系统工程: 1. 选对基础镜像——alpine(约 5MB)比 ubuntu(约 70MB)小十几倍。但 alpine 用 musl libc 而不是 glibc,某些 C 扩展可能不兼容。 2. 多阶段构建——构建在第一个阶段(大镜像),最终产物复制到第二个阶段(小镜像,如 alpine)。构建工具不进入最终镜像。 3. 减少层数——把多个 RUN 合并成一个,用 && 连接。每层都是增量,层数少=体积小。 4. 清理包缓存——apt-get 后立刻 rm -rf /var/lib/apt/lists/*。 5. 不要装不需要的东西——推荐用 --no-install-recommends。 6. .dockerignore——把 node_modules、.git、.env 排除,别打进镜像。
dockerfile
# 多阶段构建示例
# Stage 1: 构建
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: 生产
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]
多阶段构建是镜像瘦身的最佳实践——构建阶段可以很臃肿(装各种构建工具),最终镜像只用 alpine 加产出的 dist。

02. 层缓存优化——加速构建

Docker 的构建是逐层缓存——每层如果没变就用缓存,变了这一层和所有后面的层都重建。所以正确的做法是把不常变的放前面、常变的放后面。 典型的优化:先 COPY package.json 和 lock 文件,然后 RUN npm ci。最后才 COPY 源代码。这样只改代码时,npm install 那层能走缓存,构建快很多。 注意:docker build 的缓存可能过期——如果 package.json 没变但 npm registry 上的版本更新了,缓存还是会用旧版本。可以用 --no-cache 强制重建,或在 CI 里用 --build-arg 传版本信息破坏缓存。
bash
# 差的做法(每次改代码都重新 npm install)
COPY . .
RUN npm ci

# 好的做法(package.json 没变就复用缓存)
COPY package*.json ./
RUN npm ci
COPY . .

# 强制不用缓存
docker build --no-cache -t app .

# .dockerignore
git
node_modules
.env
*.md
dist

03. 容器运行时优化

镜像优化完,运行时的优化也重要: 1. 合理设置 restart policy——unless-stopped(Docker 重启/崩溃后自动启动容器),always 也行但手动 stop 后下次 Docker 重启它又活了。 2. 日志管理——不设 limit 的话容器的 stdout/stderr 日志会无限增长,撑满磁盘。用 --log-opt max-size=10m --log-opt max-file=3。 3. 优雅关闭——容器收到 SIGTERM 后要能优雅关闭。CMD 里用 exec 形式(CMD ["node", "app.js"])而不是 shell 形式(CMD node app.js),因为 shell 形式不转发信号。 4. healthcheck——给容器加健康检查,Swarm/Compose 能根据健康状态决定要不要重启容器。 5. 资源不必要的不给——--cpus 和 --memory 合理设置,别给太多也别给太少。
bash
# 日志限制
docker run -d --log-opt max-size=10m --log-opt max-file=3 nginx

# 全局日志限制(/etc/docker/daemon.json)
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

# Healthcheck(Dockerfile 里)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost/ || exit 1
json-file 是默认日志驱动,日志存在 /var/lib/docker/containers/ 下。不设置 rotate 的话几个月能长到几十 GB。

04. 多阶段构建进阶

多阶段构建不只是简单的复制文件,还能做更多: 1. 给阶段起名字:FROM image AS stage-name,后面 COPY --from=stage-name。 2. 多来源:从不同阶段甚至不同镜像复制文件。比如 Go 应用的静态编译——在构建阶段编译成二进制,运行阶段用 scratch(空白镜像)只放二进制。 3. --chown 参数:COPY --from=builder --chown=appuser:appuser /app/dist ./dist。在复制的同时换所有者。 4. 构建参数:用 ARG 传构建时变量,结合 --target 只构建到某个阶段(适合 CI 里分开构建和测试)。
dockerfile
# 多阶段构建——Go 应用从构建到 scratch
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
# 最终镜像只有 server 二进制,几 MB!

# 只构建到 builder 阶段
docker build --target builder -t app:dev .
scratch 是空的,没有 shell、没有包管理器、啥都没有。适合静态编译的 Go/Rust 应用,极致安全和小巧。

05. Docker Compose 优化

docker-compose.yml 也有一些优化点: 1. 利用 depends_on 和 healthcheck 控制启动顺序——数据库就绪后再启动应用。但 depends_on 只等容器启动不等服务就绪,配合 condition: service_healthy 才能真正等。 2. 环境变量用 .env 文件管理,不要写死在 compose 文件里。不同环境各有一个 .env 文件,compose 自动读取。 3. 合理利用 profiles——不是所有服务每次都要启动。给开发用的工具服务加 profiles: ['dev'],docker compose --profile dev up 才启动它们。 4. 限制资源——compose 里也能设 cpus 和 memory limits,防止某个容器吃光资源。 5. network 用自定义网络——多个 compose 项目默认创建各自的网络互不干扰。需要跨项目通信时指定外部网络。
yaml
# docker-compose.yml 优化
get services:
  app:
    image: myapp
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
  db:
    image: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
depends_on 的 condition: service_healthy 需要 compose v3.4+(或使用 v2.x 格式)。老版本只支持 depends_on 启动顺序不检查健康状态。

知识测验

1/5正确 0

多阶段构建的主要目的?