摘要
在本篇进阶教程中,我们将直面 Docker 镜像体积过大的普遍痛点,并学习两大核心优化策略:选择更小的基础镜像和使用多阶段构建 (Multi-stage builds)。通过一个真实的 Go 语言 Web 应用案例,你将亲眼见证如何将一个数百MB的镜像优化到不足10MB。接着,我们将揭秘 Docker Compose,一个能通过简单的 YAML
文件定义和管理多容器应用的编排利器。你将学会编写 docker-compose.yml
文件,实现一键启动、管理和连接一个包含 Web 应用和 Redis 缓存的复杂服务组合,从而大幅提升开发和测试效率。
引言:从“能用”到“好用”
在上一篇文章中,我们成功地将一个简单的 Web 应用打包成了 Docker 镜像并运行了起来。这非常棒,你已经掌握了 Docker 的基础工作流。但是,在真实的生产环境中,“能用”只是起点,“好用”才是我们的追求。
你是否思考过这些问题:
- “我构建的镜像是不是太大了?每次上传和下载都要等好久。”
- “如果我的应用依赖数据库、缓存等多个服务,每次都要手动
docker run
好几个容器,还要处理网络连接,太麻烦了!”
如果你有这些困惑,那么恭喜你,这篇文章就是为你准备的。今天,我们将学习两大 Docker 进阶神技:镜像优化和多容器编排 (Docker Compose),让你的 Docker 实践能力再上一个新台阶!
一、镜像优化黄金法则:让你的镜像“瘦身”90%
臃肿的镜像不仅浪费存储空间,更会拖慢 CI/CD 流水线中的构建、推送和拉取速度。下面我们通过一个 Go 语言的例子,来实战镜像优化。
为什么用 Go? 因为 Go 是编译型语言,它能完美地展示编译环境和运行环境分离所带来的巨大优化效果。这个思想同样适用于 Java, C++, Rust 等其他编译型语言。
1. 准备一个简单的 Go Web 应用
在你的工作区创建一个新文件夹,比如 go-docker-optimize
,并在其中创建 main.go
文件:
// main.go
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Optimized Docker Image!")
}
func main() {
http.HandleFunc("/", helloHandler)
fmt.Println("Server started at :8080")
http.ListenAndServe(":8080", nil)
}
2. “臃肿”的初版 Dockerfile
一个新手可能会这样写 Dockerfile
:
# Dockerfile.bad
FROM golang:1.19 # 使用官方的 Go 镜像作为基础,它很大 (约 800MB)
WORKDIR /app
COPY . .
# 在镜像中编译 Go 应用
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]
让我们构建它:
docker build -t go-app:bad -f Dockerfile.bad .
docker images go-app:bad
你会发现,这个镜像的大小可能在 800-900MB 左右!太可怕了。这是因为它包含了完整的 Go SDK、编译器和各种工具链,而我们运行应用时,其实只需要那个小小的、编译好的二进制文件。
3. 优化策略一:选择更小的基础镜像
一个简单的改进是使用基于 Alpine Linux 的 Go 镜像,它体积更小。
# Dockerfile.better
FROM golang:1.19-alpine
# ... 其他内容不变 ...
构建后你会发现,镜像体积可能降到了 300-400MB 左右。有进步,但还不够!
4. 优化策略二:多阶段构建 (Multi-stage builds) - 终极武器!
这是镜像优化的核心技巧。我们可以在同一个 Dockerfile
中定义多个构建阶段。第一个阶段(我们称之为 builder
)使用包含完整工具链的镜像来编译应用,第二个阶段则使用一个极小的基础镜像(比如 alpine
或 scratch
),只从第一个阶段复制我们真正需要的编译结果。
修改你的 Dockerfile
(可以就叫 Dockerfile
了),写入以下内容:
# ===== Stage 1: Build =====
# 使用官方的 Go 镜像作为构建环境,并给它起个别名 'builder'
FROM golang:1.19-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制 Go 模块文件并下载依赖 (如果你的项目有 go.mod)
# COPY go.mod ./
# COPY go.sum ./
# RUN go mod download
# 复制所有源码
COPY . .
# 编译应用,-o 指定输出文件名。CGO_ENABLED=0 是为了静态编译,避免依赖 C 库。
RUN CGO_ENABLED=0 go build -o main .
# ===== Stage 2: Run =====
# 使用一个极度精简的基础镜像
FROM alpine:latest
# 设置工作目录
WORKDIR /app
# 从 'builder' 阶段的 /app/main 路径,复制编译好的二进制文件到当前阶段的 /app/ 目录
COPY --from=builder /app/main .
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["./main"]
现在,让我们用这个终极版 Dockerfile
来构建:
docker build -t go-app:optimized .
docker images go-app:optimized
查看镜像大小,你会惊喜地发现,它的大小只有 10-15MB!
我们实现了超过 98% 的体积缩减! 这就是多阶段构建的威力。它完美地将“开发编译环境”和“生产运行环境”隔离开,只保留了运行应用所必需的最小集合。
二、多容器编排:Docker Compose 闪亮登场
现在,我们的应用镜像已经足够“苗条”了。但现代应用很少是孤立存在的,它们通常需要与数据库(如 MySQL)、缓存(如 Redis)等服务交互。
如果手动管理这些容器,你需要:
docker run redis
docker run mysql
(并配置一堆环境变量)docker run my-app
(并想办法让它连接到 redis 和 mysql 容器)
这个过程繁琐且容易出错。Docker Compose 就是为了解决这个问题而生的。它允许你使用一个 YAML
文件 (docker-compose.yml
) 来定义和运行一个多容器的 Docker 应用。
1. 案例升级:Web 应用 + Redis 缓存
让我们修改一下之前的 Node.js 应用,让它能连接到 Redis。
a. 安装 Node.js 的 Redis 客户端:
在你的 my-docker-app
文件夹(第一篇文章创建的)中,打开终端,运行:
npm install redis
这会生成 package.json
和 package-lock.json
文件。
b. 修改 app.js
:
// app.js
const http = require('http');
const { createClient } = require('redis');
const client = createClient({
// 'redis' 是我们在 docker-compose.yml 中定义的服务名
url: 'redis://redis:6379'
});
client.on('error', (err) => console.log('Redis Client Error', err));
const hostname = '0.0.0.0';
const port = 3000;
const server = http.createServer(async (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
try {
// 每次访问,计数器加 1
const counter = await client.incr('visitor_count');
res.end(`Hello, Docker Compose! You are visitor #${counter}.\n`);
} catch (err) {
res.statusCode = 500;
res.end('Failed to connect to Redis.\n');
}
});
async function startServer() {
await client.connect();
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
}
startServer();
c. 更新 Dockerfile
以安装依赖:
# Dockerfile for Node.js
FROM node:18-alpine
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装生产环境依赖
RUN npm ci --only=production
# 复制应用代码
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
2. 编写 docker-compose.yml
现在是见证奇迹的时刻。在项目根目录下,创建 docker-compose.yml
文件:
# docker-compose.yml
version: '3.8' # 指定 compose 文件版本
services: # 定义一组服务
web: # 服务名,可以自定义,比如叫 'app'
build: . # 指定构建上下文,即使用当前目录下的 Dockerfile 来构建镜像
ports:
- "8000:3000" # 端口映射,和 docker run -p 效果一样
depends_on: # 声明依赖关系
- redis
redis: # 定义另一个服务,名为 'redis'
image: "redis:alpine" # 直接使用 Docker Hub 上的官方 redis 镜像
这个文件清晰地定义了我们的应用栈:
- 一个名为
web
的服务,它通过当前目录的Dockerfile
构建,并将主机的 8000 端口映射到容器的 3000 端口。 - 一个名为
redis
的服务,它直接使用redis:alpine
官方镜像。 web
服务depends_on
redis
,这保证了redis
容器会先于web
容器启动。- 关键点:在同一个
docker-compose
网络中,容器之间可以直接通过服务名 (redis
) 作为主机名进行通信!这就是为什么app.js
中可以使用redis://redis:6379
。
3. 一键启动!
现在,只需要一条命令,就可以启动整个应用栈:
docker-compose up
(新版本的 Docker Desktop 可能推荐使用 docker compose up
,中间没有 -
)
你会看到 Docker Compose 先拉取 Redis 镜像,然后构建你的 web
应用镜像,最后依次启动 redis
和 web
两个容器。
打开浏览器访问 http://localhost:8000
,刷新几次页面,你会看到访客计数不断增加!
常用 Docker Compose 命令:
docker-compose up -d
: 后台启动并运行。docker-compose down
: 停止并移除所有相关的容器和网络。docker-compose ps
: 查看由 compose 管理的容器状态。docker-compose logs -f web
: 实时查看web
服务的日志。
总结与预告
今天,你的 Docker 技能实现了质的飞跃:
- 你掌握了通过多阶段构建等技巧,将镜像体积大幅优化的能力,这是生产环境必备的技能。
- 你学会了使用 Docker Compose 来定义和管理多容器应用,告别了繁琐的手动
docker run
,极大地提升了开发和测试效率。
至此,我们在单台机器上管理容器已经游刃有余。但是,当应用需要部署到由多台服务器组成的集群上,需要考虑跨主机的服务发现、负载均衡、故障自愈时,Docker 和 Docker Compose 就显得力不从心了。
这时,我们需要一个更强大的“容器舰队总司令”。
下一篇预告:【云原生核心技术 (5/12): 从 Docker 到 K8s——为什么你需要 Kubernetes 这个“容器管家”?】
我们将正式开启 Kubernetes (K8s) 的大门,理解它诞生的背景、核心价值和基本架构,为你解释为什么在云原生时代,K8s 是不可或缺的。准备好迎接容器编排之王吧!