「go 服务一般都怎么部署?规范是什么?Dockerfile 写到哪、怎么写?」—— 这篇讲清 go-zero 的标准部署姿势:单服务单 Dockerfile、放服务根目录、多阶段构建、配置外置、配合 docker-compose 或 K8s 编排,并给出可直接抄的源码。
先把最关键的说在前面。
go-zero 服务部署的规范可以一句话概括:每个可部署服务一个仓库(或 monorepo 里一个目录),在其根目录(和 go.mod 同级)放一份Dockerfile,用「多阶段构建」产出静态二进制,靠-f 指定配置启动,配置与密钥外置(呼应 go-zero 多环境篇)。本地用 docker-compose,生产用 Kubernetes。
从代码到运行的完整链路。
规范答案:服务根目录,和 go.mod 同级。
docker build . 的构建上下文就是根目录,Dockerfile 在根目录能让 COPY . . 一次把源码 + etc 全部拷进构建阶段,且 go.mod / go.sum 就在眼前,依赖缓存最省事。微服务里一个可部署服务 = 一个 Dockerfile,不要写成「一个 Dockerfile 编所有服务」的怪物。如果多个 go-zero 服务放在同一个大仓库里,就在每个服务的目录各放一个 Dockerfile,构建时把上下文切到对应子目录:
# 在仓库根目录执行,上下文指向子服务目录
docker build -f services/user-api/Dockerfile -t user-api:v1 services/user-api
docker build -f services/order-rpc/Dockerfile -t order-rpc:v1 services/order-rpc
一份可直接抄的生产级 Dockerfile。
# ---------- 阶段一:构建 ---------- FROM golang:1.22-alpine AS builder WORKDIR /build # 模块代理:国内用 goproxy.cn,CI 里可换成公司私服 ENV GOPROXY=https://goproxy.cn,direct ENV GOSUMDB=off # 先拷 go.mod / go.sum,利用层缓存:依赖没变就不会重下 COPY go.mod go.sum ./ RUN go mod download # 再拷全部源码并编译 COPY . . # CGO_ENABLED=0 → 静态二进制,可跑在 alpine/distroless # -ldflags "-s -w" → 去掉符号表和调试信息,镜像更小 RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o user-api user.go # ---------- 阶段二:运行 ---------- FROM alpine:3.20 WORKDIR /app # CA 证书:不装的话容器内访问 HTTPS 外部接口会报 x509 错误 # tzdata:让 Go 的 time.LoadLocation 能按城市名取时区 RUN apk add --no-cache ca-certificates tzdata ENV TZ=Asia/Shanghai # 只拷二进制 + 配置文件,运行阶段不残留任何源码 COPY --from=builder /build/user-api /app/user-api COPY --from=builder /build/etc /app/etc EXPOSE 8888 # 用 -f 指定配置启动(呼应「多环境」那篇:一份二进制多份配置) ENTRYPOINT ["/app/user-api", "-f", "/app/etc/user-api.yaml"]
CGO_ENABLED=0,alpine 里跑不了(缺 glibc);② 忘了装 ca-certificates,调用外部 HTTPS API 直接失败;③ 忘了 tzdata,日志/业务时间用的是 UTC 或报错。# order-rpc 服务:入口是 order.go,内网 RPC 端口(如 8081) FROM golang:1.22-alpine AS builder WORKDIR /build ENV GOPROXY=https://goproxy.cn,direct COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o order-rpc order.go FROM alpine:3.20 WORKDIR /app RUN apk add --no-cache ca-certificates tzdata ENV TZ=Asia/Shanghai COPY --from=builder /build/order-rpc /app/order-rpc COPY --from=builder /build/etc /app/etc EXPOSE 8081 ENTRYPOINT ["/app/order-rpc", "-f", "/app/etc/order.yaml"]
别把本地脏东西、敏感配置、构建产物塞进镜像。
# 版本控制与本地文件 .git .gitignore *.md .idea .vscode # 日志与本地产物 logs/ bin/ tmp/ # 含真实密钥的本地配置(生产用 ConfigMap/Secret,不进镜像) etc/*-prod.yaml .env *.local.yaml # 测试与文档 test/ docs/ *.test
etc/*-prod.yaml:本地那份写了真实连接串/密钥的 prod 配置不该进镜像。镜像里只留「模板」(用 ${ENV} 占位,呼应多环境篇),真实值由部署时注入。这样镜像可以安全公开/复用。命令与 tag 规范。
# 在 user-api 根目录(Dockerfile 所在目录)执行 docker build -t registry.example.com/user-api:v1.0.0 . # 推荐:同时打一个 git commit sha 的 tag,便于回滚溯源 docker build -t registry.example.com/user-api:$(git rev-parse --short HEAD) . # 推送到镜像仓库 docker push registry.example.com/user-api:v1.0.0 docker push registry.example.com/user-api:$(git rev-parse --short HEAD)
:latest 作为生产 tag——它不可追溯、不可回滚,K8s 默认还会用缓存导致「其实没更新」。生产必须打固定版本号或 git sha。go-zero 自带 goctl docker(呼应常用命令篇),能一键生成多阶段 Dockerfile,之后你按需微调即可:
# 在服务根目录执行,生成 Dockerfile
goctl docker -go 1.22 -port 8888 -home .
goctl docker 生成的 Dockerfile 默认已带多阶段 + CGO_ENABLED=0。但它不一定装 ca-certificates/tzdata,上线前请确认已加(见上面的踩坑提醒)。容器里如何传 -f 与配置(呼应多环境篇)。
# 直接用源码跑,指定 dev 配置 go run user.go -f etc/user-api.yaml # 或编译后跑 go build -o user-api user.go ./user-api -f etc/user-api.yaml
# 用 -e 注入环境变量,覆盖 yaml 里的 ${ENV} 占位 docker run -d -p 8888:8888 \ -e MYSQL_DSN='user:pass@tcp(db:3306)/user' \ -e JWT_SECRET='***' \ registry.example.com/user-api:v1.0.0 # 或用 -v 把本地的 prod 配置文件挂进容器(覆盖镜像内的 etc) docker run -d -p 8888:8888 \ -v $(pwd)/etc/user-api.prod.yaml:/app/etc/user-api.yaml \ registry.example.com/user-api:v1.0.0
ENTRYPOINT 已经写死 -f /app/etc/user-api.yaml。要换环境,要么换挂载的 yaml,要么换 ${ENV} 注入的值——不要去改 ENTRYPOINT。把服务和依赖(MySQL/Redis)一起拉起。
version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: "123456" MYSQL_DATABASE: "user_dev" ports: ["3306:3306"] healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: ["6379:6379"] user-api: build: context: . dockerfile: Dockerfile ports: ["8888:8888"] depends_on: mysql: condition: service_healthy environment: MYSQL_DSN: "root:123456@tcp(mysql:3306)/user_dev" REDIS_HOST: "redis:6379" command: ["/app/user-api", "-f", "/app/etc/user-api.yaml"]
depends_on: condition: service_healthy 等依赖真正就绪再启动服务,避免「库还没起来服务就崩」。Deployment + Service + ConfigMap + Secret 四件套(呼应多环境篇)。
apiVersion: v1 kind: ConfigMap metadata: name: user-api-config data: user-api.yaml: | Name: user-api Mode: pro Host: 0.0.0.0 Port: 8888 Mysql: DataSource: ${MYSQL_DSN} # 占位,由 Secret 注入的 env 填 CacheRedis: - Host: ${REDIS_HOST} Log: Mode: console # 容器里输出到 stdout Encoding: json
apiVersion: v1 kind: Secret metadata: name: user-api-secret type: Opaque stringData: MYSQL_DSN: "prod_user:xxxx@tcp(db.prod:3306)/user" REDIS_HOST: "redis.prod:6379" JWT_SECRET: "***"
apiVersion: apps/v1 kind: Deployment metadata: name: user-api spec: replicas: 2 selector: matchLabels: { app: user-api } template: metadata: labels: { app: user-api } spec: containers: - name: user-api image: registry.example.com/user-api:v1.0.0 ports: - containerPort: 8888 envFrom: # Secret → 容器环境变量,填 ${ENV} - secretRef: { name: user-api-secret } volumeMounts: - name: cfg mountPath: /app/etc # 覆盖镜像内的 etc resources: # 务必设 request/limit,避免抢资源 requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "500m", memory: "256Mi" } livenessProbe: # 挂了就重启 httpGet: { path: /health, port: 8888 } initialDelaySeconds: 10 periodSeconds: 15 readinessProbe: # 没就绪就不接流量 httpGet: { path: /health, port: 8888 } initialDelaySeconds: 5 periodSeconds: 10 volumes: - name: cfg configMap: name: user-api-config --- apiVersion: v1 kind: Service metadata: name: user-api spec: selector: { app: user-api } ports: - port: 8888 targetPort: 8888
-f 指向 /app/etc/user-api.yaml(ConfigMap 挂载),yaml 里的 ${MYSQL_DSN} 等由 Secret 转成的 env 填值——和多环境篇的机制完全一致。# 生成部署相关 yaml(Deployment/Service 等)
goctl kube deploy -name user-api -namespace default \
-image registry.example.com/user-api:v1.0.0 -port 8888
goctl kube deploy 会生成一套基础 Deployment+Service(呼应常用命令篇)。但 ConfigMap/Secret 分离、探针、resources 通常要你再补——生成物是起点不是终点。go-zero 默认没 /health 端点,自己加一个最稳。
package main import ( "context" "flag" "net/http" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/rest" "your_project/internal/config" "your_project/internal/handler" "your_project/internal/svc" ) var configFile = flag.String("f", "etc/user-api.yaml", "the config file") // 独立的探针端口,专供 K8s liveness/readiness 调用 func startHealthServer() { mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) // 8089 仅集群内网可达,不对外暴露业务 go http.ListenAndServe(":8089", mux) } func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c, conf.UseEnv()) startHealthServer() // 先起探针,再起主服务 server := rest.MustNewServer(c.RestConf) defer server.Stop() ctx := svc.NewServiceContext(c) handler.RegisterHandlers(server, ctx) server.Start() }
/health 里查一下 svc 里的连接;但要避免「探针打挂 DB」。简单场景返回 200 即可,依赖健康交给 readiness 的优雅处理。K8s 里把 port 指向 8089。/health。别再写容器内文件,交给容器引擎采集(呼应日志篇 / 配置篇)。
Name: user-api Mode: pro Host: 0.0.0.0 Port: 8888 Log: Mode: console # 输出到 stdout/stderr,容器平台统一采集 Encoding: json # json 格式便于 Loki / ES 解析 Level: info Compress: false
Mode:顶层的 Mode: pro 是服务运行模式(dev/test/pro,呼应多环境篇);Log.Mode: console 是日志输出方式(console/file/volume,呼应配置篇)。两者各管各的。kubectl logs、Loki、ELK 才能统一采集。若仍用 Log.Mode: file,日志写进容器文件系统,容器一重启/重建就丢,且平台采不到。照着做,上线不翻车。
| 该做 / 别做 | 说明 |
|---|---|
| ✅ Dockerfile 放服务根目录 | 与 go.mod 同级,构建上下文干净、依赖缓存最优 |
✅ 多阶段构建 + CGO_ENABLED=0 | 静态二进制,可跑 alpine/distroless,镜像小、无 glibc 依赖 |
| ✅ 运行阶段装 ca-certificates + tzdata | 否则 HTTPS 外部调用失败、时区错乱 |
| ✅ 配置外置、密钥走 Secret | 镜像只含模板(${ENV}),真实值 ConfigMap/Secret 注入(呼应多环境篇) |
| ✅ 镜像 tag 用版本号 / git sha | 禁用 :latest,可溯源可回滚 |
| ✅ 设 resources request/limit | 避免单个 Pod 吃光节点资源,调度更稳 |
| ✅ 配 liveness / readiness 探针 | 自愈 + 优雅接流量,依赖独立健康端口 |
✅ 日志 Log.Mode: console | 走 stdout 交平台采集,别写容器内文件 |
| ⚠️ 别把 prod 真实配置打进镜像 | 本地 etc/*-prod.yaml 用 .dockerignore 排除 |
| ⚠️ 别在容器里改 ENTRYPOINT | 换环境靠挂载 yaml 或 env 覆盖,不动启动命令 |
| ⚠️ 非 root 运行更安全 | 可加 USER 1000(需确保对 /app 有读权限) |