4. Docker进阶:多阶段构建、镜像优化、Docker Compose、私有仓库

好,咱们继续往下走。前面我们聊了Docker的基础操作,说白了就是怎么把应用装进容器里跑起来。但真正到了生产环境,你会发现事情没那么简单——镜像动不动几百兆,构建速度慢得像蜗牛,多个服务之间怎么编排也是个大问题。

这一章,我就把我在实战中踩过的坑、总结的经验,一次性倒给你。咱们从多阶段构建开始,一步步把镜像做小、做快、做得更专业。

4.1 多阶段构建:告别臃肿镜像

先问个问题:你见过最大的Docker镜像有多大?我记得有次接手一个Java项目,镜像居然有1.2GB。我当时就懵了,这玩意儿传到仓库得等到猴年马月?

为什么会这样?因为很多人习惯用一个Dockerfile搞定所有事——编译、测试、打包、运行,全塞在一个层里。结果就是,编译工具、临时文件、依赖缓存全留在镜像里,不胖才怪。

多阶段构建就是来解决这个问题的。它的核心思想很简单:把构建环境和运行环境分开。你想想看,你写代码需要IDE、编译器、各种库,但用户运行你的程序,只需要最终的可执行文件就够了。

核心思路:第一阶段负责编译,第二阶段只拿编译结果,重新构建一个干净的运行镜像。

来看个实际例子。这是一个Go应用的Dockerfile:

# 第一阶段:编译
FROM golang:1.20 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .

# 第二阶段:运行
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

看到没?--from=builder 这个参数是关键。它只把第一阶段编译好的二进制文件复制过来,其他乱七八糟的东西一概不要。最终镜像从1.2GB直接降到15MB,这差距,你自己品品。

我的习惯:给每个阶段起个有意义的名字,比如builder、test、production。别用默认的数字索引,不然Dockerfile一长,你自己都分不清哪个是哪个。

4.2 镜像优化:从GB到MB的实战技巧

多阶段构建只是第一步。真正要把镜像做到极致,还得掌握下面这些技巧。我在项目中优化过上百个镜像,总结下来就四个字:能省则省

4.2.1 选择最小的基础镜像

基础镜像的选择,直接决定了你的镜像下限。alpine只有5MB,ubuntu有80MB,而golang:1.20有800MB。你想想看,如果只是跑个编译好的二进制,为什么要带一整套操作系统?

基础镜像 大小 适用场景
scratch 0 MB 纯静态编译的Go、Rust应用
alpine ~5 MB 大多数通用场景
slim (如 python:3.11-slim) ~50 MB 需要部分系统库的场景
ubuntu/debian ~80 MB 需要完整系统环境的场景

我个人最常用的是alpine,但要注意一点——alpine用的是musl libc,不是glibc。有些C扩展或者动态链接库可能不兼容。我曾经就因为这个踩过坑,一个Python项目在alpine上跑不起来,查了半天才发现是某个C库的问题。

4.2.2 减少镜像层数

Docker镜像是由一层层文件系统叠加起来的。每一层都会增加最终镜像的大小。所以,能合并的命令尽量合并。

看个反面教材:

# 不推荐:每行一个RUN,产生多层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 推荐:合并成一个RUN,只产生一层
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

嗯,这里要注意:rm -rf /var/lib/apt/lists/* 一定要跟安装命令放在同一个RUN里。因为Docker的每一层都是独立的,如果你分开写,前面那层下载的包列表文件还在,只是被标记为删除,实际上仍然占用空间。

我曾经踩过的坑:有次为了图省事,把apt-get update和install分开写,结果镜像大了200MB。后来一查,原来是apt的缓存文件被保留在了中间层里。从那以后,我养成了「安装即清理」的习惯。

4.2.3 利用.dockerignore

这个太容易被忽略了。很多人直接把整个项目目录COPY进去,结果node_modules、.git、日志文件全被打包进镜像。你想想看,一个node_modules可能就有几百MB,这谁受得了?

在项目根目录创建 .dockerignore 文件:

node_modules
.git
*.log
.env
dist
.cache

这样,COPY . . 的时候,这些文件就会被自动忽略。构建速度能快不少,镜像也干净多了。

4.3 Docker Compose:一键编排多服务

单个容器好办,但实际项目往往是多个服务协同工作——前端、后端、数据库、缓存、消息队列...你总不可能一个个手动启动吧?

Docker Compose就是干这个的。它用YAML文件定义一组容器,一条命令就能全部启动。说白了,就是把你手动敲的docker run命令,写成配置文件。

来看一个典型的Web应用例子:

version: '3.8'

services:
  web:
    build: ./web
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=secret

volumes:
  pgdata:

这个文件定义了三个服务:web前端、api后端、db数据库。它们之间的依赖关系通过 depends_on 指定。启动时,Docker Compose会先启动db,再启动api,最后启动web。

常用的命令也就那么几个:

  • docker compose up -d:后台启动所有服务
  • docker compose down:停止并删除所有容器
  • docker compose logs -f:实时查看所有服务的日志
  • docker compose exec api sh:进入api容器执行命令

我的建议:开发环境用Compose,生产环境用Kubernetes。Compose适合单机编排,K8s适合集群编排。别想着用Compose去管理上百个服务,那不是它的强项。

4.4 私有仓库:你的镜像保险箱

镜像构建好了,总得有个地方存吧?Docker Hub虽然方便,但毕竟是公共的。企业项目、敏感数据,谁敢往上面放?

私有仓库就是你的私有镜像存储中心。最常用的方案是Harbor,它功能全面,支持权限管理、镜像复制、漏洞扫描。如果只是小团队用,Registry 2.0也够了。

4.4.1 搭建一个简单的私有仓库

用Docker启动一个Registry 2.0,就两行命令:

docker run -d -p 5000:5000 --name registry registry:2

然后就可以推送镜像了:

docker tag myapp:latest localhost:5000/myapp:latest
docker push localhost:5000/myapp:latest

就这么简单。但要注意,默认的Registry是HTTP的,Docker客户端默认只允许HTTPS。如果你是在本地测试,需要配置Docker的insecure-registries:

# /etc/docker/daemon.json
{
  "insecure-registries": ["localhost:5000"]
}

我曾经踩过的坑:有次在生产环境忘了配HTTPS,结果镜像死活推不上去。查了半天才发现是安全策略的问题。所以,生产环境一定要配HTTPS证书,别图省事用HTTP。

4.4.2 使用Harbor管理镜像

如果团队规模大了,我建议直接用Harbor。它提供了Web界面,可以创建项目、管理用户、设置权限。比如,你可以让开发人员只能推送镜像到dev项目,而生产环境的镜像只有运维人员才能操作。

Harbor的安装也不复杂,官方提供了在线安装器:

wget https://github.com/goharbor/harbor/releases/download/v2.8.0/harbor-online-installer-v2.8.0.tgz
tar xvf harbor-online-installer-v2.8.0.tgz
cd harbor
cp harbor.yml.tmpl harbor.yml
# 修改配置文件,设置hostname、密码等
./install.sh

安装完成后,访问 https://your-harbor-host 就能看到登录界面了。默认管理员账号是admin,密码在harbor.yml里配置。

4.5 实战:构建一个优化的Node.js应用镜像

光说不练假把式。咱们把前面学的知识串起来,优化一个Node.js应用。

先看原始的Dockerfile:

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

这个镜像有多大?大概1.2GB。我们来优化它:

# 第一阶段:安装依赖
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 第二阶段:构建运行镜像
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

优化后,镜像大小降到了150MB左右。如果再加上.dockerignore,还能再小一些。你想想看,从1.2GB到150MB,部署速度能快多少?

好了,这一章的内容就到这。多阶段构建、镜像优化、Compose编排、私有仓库,这些都是在生产环境中必须掌握的技能。下一章,咱们聊聊Kubernetes,那才是真正的容器编排大杀器。