我第一次接手一个"成熟"的 Go 项目时,光是让它在本地跑起来就花了一整个下午。docker compose up 之后,终端疯狂报错,数据库连接被拒绝、Redis 连不上、Kafka 还没就绪。翻了一圈文档,发现前人留下的"解决方案"是:在启动脚本里加了个 sleep 10。
后来同事告诉我,这个 sleep 从 2 秒一直加到 10 秒,因为"有时候跑得起来,有时候跑不起来"。更糟的是,这个 compose.yml 文件已经膨胀到快 300 行,里面有五六个服务、一堆硬编码的环境变量,还有三个被注释掉的配置块------没人敢删。
这不是个别现象,是我见过的绝大多数 Go 项目的真实写照。
如果你也有类似的经历,下面这六个模式能帮你在不跟 Docker Compose 较劲的前提下,让本地开发环境稳定、测试可靠、构建高效。
模式一:健康检查比"等几秒"靠谱一万倍
depends_on 的语义是"等容器启动",不是"等服务就绪"。Postgres 容器起来了,但数据库引擎可能还在初始化,你的 Go 程序一启动就执行 db.Ping(),结果只能是崩溃。
正确的做法是告诉 Compose:"等到这个服务真正能干活了,再启动我的应用。"
yaml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypass
POSTGRES_DB: mydb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
app:
build: .
depends_on:
postgres:
condition: service_healthy
这样,应用会一直等到 Postgres 真正可以接受连接才启动。同样的方法适用于任何支持健康检查的服务:Redis、MySQL、Kafka------全都通用。
模式二:别把所有服务塞进一个文件
一个 compose.yml 管所有,意味着每次启动都要拉起整个架构。新人想改一行代码,结果启动了数据库、缓存、消息队列、监控面板、Worker 进程------这些东西他一个都不需要。
分层配置是更好的做法:
compose.yaml # 基础设施:Postgres、Redis
compose.dev.yaml # 开发专用:挂载源码、热加载、调试端口
compose.test.yaml # 测试专用:临时数据库、独立端口
compose.override.yaml # 个人配置,不提交
bash
# docker-compose.dev.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
ports:
- "8080:8080"
- "2345:2345" # delve debugger
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
这样,新人跑 docker compose -f compose.yaml -f compose.dev.yaml up 就能开工,不会被多余的组件干扰。
模式三:多阶段构建让镜像从 1GB 缩到 20MB
很多项目的 Dockerfile 直接从 golang:1.23 开始,把整个编译工具链塞进最终镜像。结果就是 1.2GB 的镜像,CI 推一次等三分钟。
合理的 Go Dockerfile 应该分层:
dockerfile
# ---- 依赖缓存 ----
FROM golang:1.23-alpine AS deps
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# ---- 构建 ----
FROM deps AS builder
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
# ---- 运行 ----
FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
这样做的效果:依赖缓存不会因为改了一行业务代码就失效;最终镜像只包含一个静态二进制文件,大多数 Go 服务的镜像能压缩到 20MB 以内。
模式四:用 .env 管配置,别往 YAML 里写密码
POSTGRES_PASSWORD: admin123 这种写法,迟早会出事。密码泄露只是时间问题。
Docker Compose 原生支持 .env 文件,你只需要把配置写进去:
# .env(本地开发,不提交)
POSTGRES_USER=dev
POSTGRES_PASSWORD=devpass
POSTGRES_DB=devdb
提交一个 .env.example 作为模板就够了。新人复制、改名、填空,就能跑起来。
模式五:测试数据库跟开发数据库分开
集成测试直接跑在开发数据库上,是在玩火。一个清空表的测试用例就能让你一整个下午的调试数据灰飞烟灭。
测试环境应该独立,而且最好用内存存储:
yaml
# compose.test.yaml
services:
test_db:
image: postgres:16-alpine
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser"]
interval: 2s
timeout: 2s
retries: 10
tmpfs 让数据跑在内存里,测试结束数据自动消失,速度比磁盘快一个量级。
总结
这些模式的核心思路只有一条:把不确定性变成确定性。
健康检查让启动顺序可靠,分层配置让环境可维护,多阶段构建让镜像高效,环境变量让配置安全,隔离测试让数据安全,Makefile 让操作简单。
一个合格的 Go 项目目录,看起来应该是这样的:
.
├── cmd/server/main.go
├── compose.yaml
├── compose.dev.yaml
├── compose.test.yaml
├── Dockerfile
├── .env.example
└── scripts/test.sh
这些东西不花哨,但它能保证:三个月后,你或者你的同事,不用再对着 200 行的 compose.yml 发愁。