go-zero 服务怎么部署

「go 服务一般都怎么部署?规范是什么?Dockerfile 写到哪、怎么写?」—— 这篇讲清 go-zero 的标准部署姿势:单服务单 Dockerfile、放服务根目录、多阶段构建、配置外置、配合 docker-compose 或 K8s 编排,并给出可直接抄的源码。

一句话结论 部署架构 目录与位置 Dockerfile 怎么写 .dockerignore 构建与推送 启动方式 docker-compose Kubernetes 健康检查 日志与 stdout 规范与踩坑

一句话结论

先把最关键的说在前面。

go-zero 服务部署的规范可以一句话概括:每个可部署服务一个仓库(或 monorepo 里一个目录),在其根目录(和 go.mod 同级)放一份Dockerfile,用「多阶段构建」产出静态二进制,靠-f 指定配置启动,配置与密钥外置(呼应 go-zero 多环境篇)。本地用 docker-compose,生产用 Kubernetes

go-zero 是「编译成单个二进制」的框架,没有 JVM、没有解释器、没有一堆运行时依赖——所以它天生适合容器化:镜像里只需要二进制 + 配置文件 + CA 证书,几十 MB 就能跑。

标准部署架构

从代码到运行的完整链路。

源码 + go.mod git 仓库 docker build CI / 本地 镜像仓库 registry / harbor Kubernetes Pod 容器: /app/user-api ConfigMap 挂 /app/etc Secret → 容器 env 探针 / 访问 MySQL·Redis·etcd
① 编译
go build 出一个静态二进制(CGO_ENABLED=0),零运行时依赖
② 打包
Dockerfile 多阶段构建,只把二进制 + etc + CA 证书塞进小镜像
③ 编排
compose 跑本地/dev,K8s 跑 test/prod,配置与密钥外置

Dockerfile 写到哪

规范答案:服务根目录,和 go.mod 同级。

推荐目录结构(goctl 生成的服务自带这种布局)

user-api/ # 服务根目录(= 构建上下文 build context) ├── etc/ │ ├── user-api.yaml # 默认/模板配置(进镜像) │ └── user-api.prod.yaml # 生产模板(进镜像,真实值用 env 覆盖) ├── internal/ │ ├── config/ │ ├── handler/ │ ├── logic/ │ ├── svc/ │ └── types/ ├── user.go # 服务入口(goctl 生成,go build 的 main 包) ├── go.mod ├── go.sum └── Dockerfile # ← 放这里!服务根目录 └── .dockerignore # ← 放这里!和 Dockerfile 同级
为什么放根目录?因为 docker build . 的构建上下文就是根目录,Dockerfile 在根目录能让 COPY . . 一次把源码 + etc 全部拷进构建阶段,且 go.mod / go.sum 就在眼前,依赖缓存最省事。微服务里一个可部署服务 = 一个 Dockerfile,不要写成「一个 Dockerfile 编所有服务」的怪物。

monorepo 多服务怎么办

如果多个 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
CI 里常见做法:哪个服务目录的 commit 有变化,就只 build 那个服务的镜像(路径过滤),避免全量构建。

Dockerfile 怎么写(多阶段构建)

一份可直接抄的生产级 Dockerfile。

① 完整 Dockerfile(rest 服务示例)

# ---------- 阶段一:构建 ----------
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 或报错。

② zrpc 服务几乎一样,只差端口和启动文件

# 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"]
zrpc 服务一般不对外暴露,容器端口只在集群内网可达;网关/API 服务才需要对外。这也是 go-zero 微服务分层的一贯约定(呼应微服务篇)。

.dockerignore 怎么写

别把本地脏东西、敏感配置、构建产物塞进镜像。

根目录放一份 .dockerignore

# 版本控制与本地文件
.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 规范。

构建并打 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

想偷懒?用 goctl 生成 Dockerfile

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

Docker 本地跑(覆盖配置/环境变量)

# 用 -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
容器内启动靠 Dockerfile 的 ENTRYPOINT 已经写死 -f /app/etc/user-api.yaml。要换环境,要么换挂载的 yaml,要么换 ${ENV} 注入的值——不要去改 ENTRYPOINT

docker-compose:dev / test 一把梭

把服务和依赖(MySQL/Redis)一起拉起。

docker-compose.yaml 示例

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"]
compose 适合开发联调 / 单机测试 / CI 里跑集成测试。生产还是交给 K8s(有自愈、扩缩容、滚动更新)。compose 里用 depends_on: condition: service_healthy 等依赖真正就绪再启动服务,避免「库还没起来服务就崩」。

Kubernetes 部署

Deployment + Service + ConfigMap + Secret 四件套(呼应多环境篇)。

① ConfigMap:放「非敏感」配置模板

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

② Secret:放「敏感」值

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: "***"

③ Deployment:挂配置 + 注入 env + 探针

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
整套思路:镜像一次构建,dev/test/prod 只换 ConfigMap + Secret,完全贴合 12-factor。go-zero 的 -f 指向 /app/etc/user-api.yaml(ConfigMap 挂载),yaml 里的 ${MYSQL_DSN} 等由 Secret 转成的 env 填值——和多环境篇的机制完全一致。

④ 想偷懒?用 goctl 生成 K8s yaml

# 生成部署相关 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 端点,自己加一个最稳。

起一个独立的轻量 health server(不依赖 go-zero 内部 API)

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()
}
探针里若想做「依赖可达性」检查(DB/Redis 连得上才算就绪),可在 /health 里查一下 svc 里的连接;但要避免「探针打挂 DB」。简单场景返回 200 即可,依赖健康交给 readiness 的优雅处理。K8s 里把 port 指向 8089。
不要把业务端口 8888 既当流量口又当探针口混用也行,但为了隔离(探针高频、且失败语义不同),独立端口更清晰。两条 probe 可以共用同一个 /health

日志输出到 stdout(容器最佳实践)

别再写容器内文件,交给容器引擎采集(呼应日志篇 / 配置篇)。

在 etc 配置里把 Log.Mode 设为 console

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,呼应配置篇)。两者各管各的。
容器里日志走 stdout,K8s 的 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 有读权限)
一句话记忆:「根目录一份 Dockerfile、多阶段、零依赖、配置外置、探针 + stdout」。这就是 go-zero 服务在容器时代的部署范式。