Appearance
Docker Compose 多服务编排
本篇是 Docker + Go 系列教程的第三篇。前两篇我们专注于把单个 Go 应用容器化,但真实的业务系统很少是单服务架构——一个 Web 应用通常依赖 MySQL、Redis、消息队列等多个服务。手动用 docker run 启动这些服务并配置网络、依赖顺序非常繁琐。Docker Compose 正是解决这个问题的工具:用一个 YAML 文件定义整个多服务环境,一条命令启动/停止全部服务。本篇将系统讲解 Docker Compose 的用法,并最终搭建一个 Go + MySQL + Redis 的三服务编排示例。
一、Docker Compose 简介
1. 什么是 Docker Compose
Docker Compose 是 Docker 官方提供的多容器编排工具。它通过一个 docker-compose.yml 文件描述:
- 有哪些服务(service)
- 每个服务用什么镜像、怎么构建
- 服务之间的依赖关系
- 网络、数据卷、端口映射
- 环境变量、健康检查
然后用一条命令 docker compose up 启动整个环境,docker compose down 销毁整个环境。
2. 适用场景
Docker Compose 主要用于:
- 本地开发环境:一键拉起依赖的所有服务,开发者无需在本地装 MySQL、Redis。
- 自动化测试:CI 中用 Compose 启动测试环境,跑完销毁。
- 小规模生产部署:单机多服务场景(不适用于需要集群编排的场景,那是 Kubernetes 的领域)。
- 演示与培训:让别人一键跑起你的项目。
3. 版本说明
- Docker Compose v1:独立 Python 程序,命令是
docker-compose(带连字符)。 - Docker Compose v2:Go 编写的插件,命令是
docker compose(空格),集成在 Docker CLI 中。
本篇使用 v2 语法(docker compose),Docker Desktop 和新版 Docker Engine 已默认集成。如果你的环境只有 v1,建议升级。
4. 与 Kubernetes 的关系
Docker Compose 和 Kubernetes 都是「容器编排」工具,但定位完全不同:
| 特性 | Docker Compose | Kubernetes |
|---|---|---|
| 定位 | 单机多容器编排 | 多机集群编排 |
| 复杂度 | 低 | 高 |
| 学习曲线 | 几小时 | 几周到几个月 |
| 适用规模 | 几个到几十个容器 | 上千个容器 |
| 高可用 | 无(单点) | 内置 |
| 自愈能力 | 无(可加 restart) | 内置 |
| 服务发现 | 通过服务名 | 内置(DNS) |
| 滚动更新 | 有限支持 | 完整支持 |
简单说:本地开发和单机部署用 Compose,生产集群用 Kubernetes。
二、docker-compose.yml 结构详解
1. 顶层结构
一个 docker-compose.yml 文件顶层有以下几个关键字段:
yaml
# Compose 文件版本(v2 推荐省略,用默认最新)
version: "3.9"
# 服务定义(核心)
services:
web:
image: nginx:alpine
db:
image: mysql:8
# 数据卷定义
volumes:
db_data:
# 网络定义
networks:
frontend:
backend:主要字段:
services:定义各个服务,必填。volumes:定义命名数据卷,供服务挂载。networks:定义网络,供服务接入。configs:定义配置(Swarm 模式用)。secrets:定义密钥(Swarm 模式用)。
2. 服务配置项
一个服务可以配置的常见字段:
| 字段 | 作用 |
|---|---|
image | 使用的镜像 |
build | 构建配置(替代 image) |
container_name | 容器名 |
ports | 端口映射 |
environment | 环境变量 |
env_file | 从文件读取环境变量 |
volumes | 数据卷挂载 |
networks | 接入的网络 |
depends_on | 依赖的服务 |
healthcheck | 健康检查 |
restart | 重启策略 |
command | 覆盖默认启动命令 |
entrypoint | 覆盖入口程序 |
working_dir | 工作目录 |
user | 运行用户 |
tty | 分配伪终端 |
stdin_open | 保持 stdin 打开 |
下面逐个讲解最常用的配置。
三、定义多个服务
下面从零开始,搭建一个 Go 应用 + MySQL + Redis 的多服务环境。
1. 基础结构
yaml
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
mysql:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=root123
- MYSQL_DATABASE=myapp
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"这三个服务组成一个最小可用环境:app 依赖 mysql 和 redis,通过服务名(mysql、redis)作为主机名访问它们。
2. 服务间通信
Compose 会为同一 docker-compose.yml 中的服务自动创建一个默认网络,服务之间可以用服务名作为主机名互相访问。例如 Go 应用连接 MySQL:
go
dsn := fmt.Sprintf("root:root123@tcp(mysql:3306)/myapp?parseTime=true")
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})注意 DSN 里的 mysql:3306——mysql 就是服务名,Compose 内置 DNS 会把它解析为对应容器的 IP。
3. 端口映射
ports 把容器端口映射到宿主机。两种写法:
yaml
ports:
- "8080:8080" # 宿主机 8080 -> 容器 8080
- "9090:80" # 宿主机 9090 -> 容器 80
- "127.0.0.1:3306:3306" # 只绑定到 localhost注意:MySQL、Redis 这种服务,如果只供 Compose 内部其他服务访问,不需要映射端口到宿主机。映射出去反而有安全风险(开发环境可以映射方便用 GUI 工具连)。
四、服务依赖与健康检查
1. depends_on 的问题
depends_on 只保证「依赖的服务容器已经启动」,但不保证服务已经就绪。例如:
yaml
services:
app:
depends_on:
- mysqlMySQL 容器启动很快,但 MySQL 服务真正能接受连接需要几秒(初始化数据库、用户等)。如果 app 在 MySQL 还没就绪时就连接,会失败。
2. 用 healthcheck + depends_on 解决
正确做法是给依赖服务配置 healthcheck,然后 depends_on 用 condition: service_healthy:
yaml
services:
app:
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: myapp
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5depends_on 的三种 condition:
service_started:容器启动即认为满足(默认)。service_healthy:容器健康检查通过才算满足。service_completed:容器运行结束(退出码 0)才算满足,用于一次性任务。
3. Go 应用内的重试机制
即便有 healthcheck,生产代码里也应该有重试逻辑,因为网络抖动、服务重启等情况都可能导致瞬时连接失败:
go
package main
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func connectDB(dsn string, maxRetries int) (*gorm.DB, error) {
var db *gorm.DB
var err error
for i := 0; i < maxRetries; i++ {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err == nil {
sqlDB, _ := db.DB()
if err = sqlDB.Ping(); err == nil {
return db, nil
}
}
fmt.Printf("Connect DB failed (attempt %d/%d): %v\n", i+1, maxRetries, err)
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("after %d retries: %w", maxRetries, err)
}
func main() {
dsn := "root:root123@tcp(mysql:3306)/myapp?parseTime=true&charset=utf8mb4"
db, err := connectDB(dsn, 10)
if err != nil {
panic(err)
}
fmt.Println("Database connected!")
_ = db
}五、环境变量管理
1. environment 直接写
yaml
services:
app:
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=root123
- DB_NAME=myapp
- REDIS_HOST=redis
- REDIS_PORT=6379也可以用 map 写法(更清晰):
yaml
services:
app:
environment:
DB_HOST: mysql
DB_PORT: "3306"
DB_USER: root
DB_PASSWORD: root123
DB_NAME: myapp2. .env 文件
把敏感信息放 .env 文件(不提交到 git),Compos 会自动加载同目录下的 .env:
text
# .env
DB_ROOT_PASSWORD=root123
DB_NAME=myapp
REDIS_PASSWORD=
APP_PORT=8080在 docker-compose.yml 中用 ${VAR} 引用:
yaml
services:
app:
ports:
- "${APP_PORT}:8080"
environment:
DB_PASSWORD: ${DB_ROOT_PASSWORD}
DB_NAME: ${DB_NAME}
mysql:
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}也可以用 env_file 加载多个文件:
yaml
services:
app:
env_file:
- .env.common
- .env.development3. 多环境配置
通过 ${VAR:-default} 提供默认值:
yaml
services:
app:
image: myapp:${TAG:-latest}
environment:
DB_PASSWORD: ${DB_PASSWORD:-defaultpass}调用时指定:
bash
TAG=v1.2.3 DB_PASSWORD=secret docker compose up六、数据卷挂载
1. 为什么需要数据卷
容器是临时的,删除容器后容器内可写层的数据会丢失。对于数据库这类需要持久化的服务,必须把数据放在数据卷(volume)中。
2. 命名卷
在顶层 volumes 声明,服务中挂载:
yaml
services:
mysql:
image: mysql:8
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:命名卷由 Docker 管理,删除容器后卷依然存在,docker compose down -v 才会删除卷。
3. 绑定挂载(bind mount)
把宿主机目录直接挂载到容器,常用于开发环境源码热重载:
yaml
services:
app:
build: .
volumes:
- ./:/app # 当前目录挂载到容器 /app
- /app/.git # 排除(用匿名卷覆盖)
working_dir: /app
command: air./:/app 把宿主机当前目录映射到容器 /app,容器内对 /app 的修改会同步到宿主机,反之亦然。
4. 常见挂载场景
yaml
volumes:
# 命名卷:持久化数据
- mysql_data:/var/lib/mysql
# 绑定挂载:开发时挂源码
- ./src:/app/src
# 只读挂载:配置文件只读
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
# 匿名卷:屏蔽容器内目录
- /app/node_modules七、网络配置
1. 默认网络
不显式配置时,Compose 会创建一个名为 <项目名>_default 的 bridge 网络,所有服务都在这个网络里,可以用服务名互相访问。
2. 自定义网络
yaml
services:
app:
networks:
- frontend
- backend
mysql:
networks:
- backend
redis:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 内部网络,不能访问外网这种隔离设计让 nginx 只能访问 app(通过 frontend 网络),不能直接访问 mysql/redis(它们在 backend 网络,且 backend 是 internal)。
3. 网络配置项
yaml
networks:
mynet:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1一般用默认配置即可,特殊需求才需要自定义子网。
八、常用命令
1. 启动与停止
bash
# 前台启动(日志直接输出到终端)
docker compose up
# 后台启动
docker compose up -d
# 启动并强制重新构建镜像
docker compose up -d --build
# 只启动指定服务(及其依赖)
docker compose up -d app
# 停止并删除容器、网络(保留卷)
docker compose down
# 停止并删除容器、网络、卷(慎用,会删数据)
docker compose down -v
# 只停止,不删除
docker compose stop
# 重新启动
docker compose start
docker compose restart2. 查看状态
bash
# 查看运行中的服务
docker compose ps
# 查看所有服务(包括已停止的)
docker compose ps -a
# 查看日志
docker compose logs
docker compose logs -f # 实时跟踪
docker compose logs -f app # 只看 app 服务
docker compose logs --tail=100 app3. 在运行中的服务执行命令
bash
# 进入 app 服务的 shell
docker compose exec app sh
# 在 mysql 服务执行 SQL
docker compose exec mysql mysql -uroot -proot123 myapp -e "SHOW TABLES;"
# 在 app 服务执行 Go 命令
docker compose exec app go test ./...4. 构建与镜像
bash
# 构建或重建服务镜像
docker compose build
# 构建时不使用缓存
docker compose build --no-cache
# 查看服务使用的镜像
docker compose images5. 其他
bash
# 查看服务配置(解析后的最终 yml)
docker compose config
# 查看服务进程
docker compose top
# 暂停服务(不释放内存)
docker compose pause
docker compose unpause九、开发环境热重载:air + volume 挂载
开发时每次改代码都要重新构建镜像非常低效。利用 volume 挂载 + air 可以实现热重载:在容器内监听文件变化,自动重新编译运行。
1. 安装 air
在项目中创建 .air.toml 配置文件:
toml
# .air.toml
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ."
bin = "./tmp/main"
full_bin = "./tmp/main"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["tmp", "vendor", "frontend"]
include_dir = []
delay = 500
[log]
time = true2. Dockerfile.dev(开发用)
dockerfile
FROM golang:1.22-alpine
WORKDIR /app
# 安装 air
RUN go install github.com/cosmtrek/air@latest
# 先拷依赖
COPY go.mod go.sum ./
RUN go mod download
# 源码通过 volume 挂载,不 COPY
CMD ["air", "-c", ".air.toml"]3. docker-compose.yml(开发版)
yaml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
volumes:
- ./:/app # 源码挂载
- go_mod:/go/pkg/mod # Go 模块缓存持久化
environment:
DB_HOST: mysql
DB_PASSWORD: root123
REDIS_HOST: redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: myapp
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
mysql_data:
go_mod:启动:
bash
docker compose up -d
docker compose logs -f app
# 改一行代码,几秒后容器内的 air 自动重新编译并重启十、多环境配置:docker-compose.override.yml
Compose 默认会读取两个文件:
docker-compose.yml:基础配置。docker-compose.override.yml:覆盖配置(如果存在)。
两者会自动合并,override 文件的配置覆盖基础配置。利用这个机制可以实现多环境配置。
1. 基础文件 docker-compose.yml
yaml
services:
app:
build: .
environment:
DB_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
volumes:
mysql_data:2. 开发覆盖 docker-compose.override.yml
yaml
services:
app:
build:
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
volumes:
- ./:/app
command: air
mysql:
ports:
- "3306:3306"
redis:
ports:
- "6379:6379"执行 docker compose up,会自动合并这两个文件,开发环境带端口映射、热重载、源码挂载。
3. 生产覆盖 docker-compose.prod.yml
yaml
services:
app:
image: myregistry/myapp:v1.2.3
restart: always
ports:
- "80:8080"
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
mysql:
restart: always
ports: !reset [] # 清空端口映射通过 -f 指定文件:
bash
# 生产环境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d4. 环境变量驱动
也可以用环境变量在一份文件中区分环境:
yaml
services:
app:
image: myapp:${TAG:-latest}
restart: ${RESTART_POLICY:-no}
ports:
- "${APP_PORT:-8080}:8080"bash
# 开发
docker compose up
# 生产
TAG=v1.2.3 RESTART_POLICY=always APP_PORT=80 docker compose up -d十一、完整示例:Go + MySQL + Redis 三服务编排
下面给出一个完整的、可直接运行的三服务编排示例。
1. 项目结构
text
compose-demo/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── .env
├── .dockerignore
└── sql/
└── init.sql2. main.go(完整可运行)
go
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type App struct {
db *gorm.DB
rdb *redis.Client
ctx context.Context
}
func (a *App) healthHandler(c *gin.Context) {
// 检查 MySQL
sqlDB, err := a.db.DB()
dbOK := "ok"
if err != nil || sqlDB.Ping() != nil {
dbOK = "fail"
}
// 检查 Redis
redisOK := "ok"
if a.rdb.Ping(a.ctx).Err() != nil {
redisOK = "fail"
}
status := http.StatusOK
if dbOK != "ok" || redisOK != "ok" {
status = http.StatusServiceUnavailable
}
c.JSON(status, gin.H{
"db": dbOK,
"redis": redisOK,
})
}
func (a *App) createUserHandler(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := a.db.Create(&u).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 清除缓存
a.rdb.Del(a.ctx, "users:all")
c.JSON(http.StatusCreated, u)
}
func (a *App) listUsersHandler(c *gin.Context) {
// 先查缓存
cached, err := a.rdb.Get(a.ctx, "users:all").Result()
if err == nil {
var users []User
if json.Unmarshal([]byte(cached), &users) == nil {
c.JSON(http.StatusOK, gin.H{"users": users, "source": "cache"})
return
}
}
// 缓存未命中,查数据库
var users []User
if err := a.db.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 写缓存
data, _ := json.Marshal(users)
a.rdb.Set(a.ctx, "users:all", data, 30*time.Second)
c.JSON(http.StatusOK, gin.H{"users": users, "source": "db"})
}
func connectDB(dsn string) (*gorm.DB, error) {
var db *gorm.DB
var err error
for i := 0; i < 15; i++ {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err == nil {
sqlDB, _ := db.DB()
if err = sqlDB.Ping(); err == nil {
return db, nil
}
}
fmt.Printf("DB connect retry %d: %v\n", i+1, err)
time.Sleep(2 * time.Second)
}
return nil, err
}
func connectRedis(addr string) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{Addr: addr})
for i := 0; i < 15; i++ {
if err := client.Ping(context.Background()).Err(); err == nil {
return client, nil
} else {
fmt.Printf("Redis connect retry %d: %v\n", i+1, err)
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("redis connect failed")
}
func main() {
dbHost := getEnv("DB_HOST", "mysql")
dbPort := getEnv("DB_PORT", "3306")
dbUser := getEnv("DB_USER", "root")
dbPass := getEnv("DB_PASSWORD", "root123")
dbName := getEnv("DB_NAME", "myapp")
redisHost := getEnv("REDIS_HOST", "redis")
redisPort := getEnv("REDIS_PORT", "6379")
port := getEnv("PORT", "8080")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4",
dbUser, dbPass, dbHost, dbPort, dbName)
db, err := connectDB(dsn)
if err != nil {
panic(err)
}
db.AutoMigrate(&User{})
rdb, err := connectRedis(fmt.Sprintf("%s:%s", redisHost, redisPort))
if err != nil {
panic(err)
}
app := &App{db: db, rdb: rdb, ctx: context.Background()}
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.GET("/health", app.healthHandler)
r.POST("/users", app.createUserHandler)
r.GET("/users", app.listUsersHandler)
fmt.Printf("Server starting on port %s\n", port)
r.Run(":" + port)
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}3. go.mod
go
module compose-demo
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/redis/go-redis/v9 v9.4.0
gorm.io/driver/mysql v1.5.4
gorm.io/gorm v1.25.5
)4. Dockerfile
dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app .
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /build/app /app/app
ENV TZ=Asia/Shanghai
EXPOSE 8080
ENTRYPOINT ["/app/app"]5. sql/init.sql
sql
-- 数据库由 MYSQL_DATABASE 环境变量自动创建
-- 这里只放初始化数据
USE myapp;
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');6. .env
text
DB_PASSWORD=root123
DB_NAME=myapp
APP_PORT=80807. docker-compose.yml
yaml
services:
app:
build: .
ports:
- "${APP_PORT:-8080}:8080"
environment:
DB_HOST: mysql
DB_PORT: "3306"
DB_USER: root
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
REDIS_HOST: redis
REDIS_PORT: "6379"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
mysql_data:
redis_data:8. 启动与测试
bash
# 启动
docker compose up -d --build
# 查看状态
docker compose ps
# NAME STATUS PORTS
# compose-demo-app-1 Up (healthy) 0.0.0.0:8080->8080/tcp
# compose-demo-mysql-1 Up (healthy) 0.0.0.0:3306->3306/tcp
# compose-demo-redis-1 Up (healthy) 0.0.0.0:6379->6379/tcp
# 健康检查
curl http://localhost:8080/health
# {"db":"ok","redis":"ok"}
# 查询用户(首次走 DB)
curl http://localhost:8080/users
# {"users":[...],"source":"db"}
# 再次查询(走缓存)
curl http://localhost:8080/users
# {"users":[...],"source":"cache"}
# 创建用户
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"charlie@example.com"}'
# 查看日志
docker compose logs -f app
# 进入 MySQL
docker compose exec mysql mysql -uroot -proot123 myapp
# 停止
docker compose down
# 停止并删除数据
docker compose down -v十二、小结
本篇系统讲解了 Docker Compose 的用法。
关键要点:
- Compose 解决多服务编排:一份 YAML 描述整个环境,一条命令启停。
- 服务依赖与健康检查:用
depends_on+condition: service_healthy保证依赖就绪,但应用内仍要有重试逻辑。 - 环境变量管理:用
.env文件分离敏感信息,${VAR:-default}提供默认值。 - 数据卷:命名卷持久化数据,绑定挂载做开发热重载。
- 网络:默认网络即可满足大多数需求,复杂场景用自定义网络隔离。
- 热重载:air + volume 挂载让开发体验接近本地。
- 多环境:用
docker-compose.override.yml区分开发/生产配置。
下一篇我们将讨论容器化 Go 应用的最佳实践,包括十二要素应用、配置管理、日志处理、优雅关停、安全加固等生产级话题。