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,那才是真正的容器编排大杀器。