← 返回博客
go2026-09-12 20:30:552 分钟 · 607 0

Gin 生产部署:配置管理、优雅停机与 Docker + Nginx

用 viper 管理多环境配置,实现信号驱动的优雅停机,再给一套多阶段 Dockerfile 和 nginx 反代,把 Gin 服务真正上线。

#docker#nginx#gin#部署

前面七篇把功能写完了,这一篇让它能在服务器上稳稳跑。三件事:配置别写死、进程被杀时不丢正在处理的请求、用容器和反向代理接公网。

1. 用 viper 管配置

config.yaml

server:
  port: "8080"
mysql:
  dsn: "root:pass@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
jwt:
  secret: "change-me"

读配置统一走一个包:

import "github.com/spf13/viper"

type Config struct {
    Server struct{ Port string }
    MySQL  struct{ DSN string }
    JWT    struct{ Secret string }
}

func LoadConfig() *Config {
    v := viper.New()
    v.SetConfigName("config")
    v.SetConfigType("yaml")
    v.AddConfigPath(".")
    v.AutomaticEnv() // 环境变量覆盖文件,部署时注入密钥
    v.ReadInConfig()

    var c Config
    v.Unmarshal(&c)
    return &c
}

AutomaticEnvJWT_SECRET 环境变量直接覆盖 jwt.secret,密钥就不进配置文件、不进镜像。本地用 yaml,生产用环境变量注入,一套代码两处生效。

2. 优雅停机

r.Run() 是阻塞的,且不会处理退出信号。用 http.Server 包一层,Shutdown 先停收新请求、等存量请求跑完再退出:

srv := &http.Server{
    Addr:    ":" + cfg.Server.Port,
    Handler: r,
}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("服务启动失败: %v", err)
    }
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 阻塞,等 Ctrl+C 或 docker stop

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Printf("强制退出: %v", err)
}
log.Println("服务已优雅关闭")

docker stop 发的是 SIGTERM,这串代码保证滚动发布、重启时不丢请求。

3. 多阶段 Dockerfile

编译放第一阶段,运行阶段只留二进制,镜像能压到 10MB 出头:

FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=build /app/server .
COPY config.yaml .
EXPOSE 8080
CMD ["./server"]

CGO_ENABLED=0 编出静态二进制,才能跑在 distroless 这种无 libc 的镜像里。go.sum 单独 COPY 利用层缓存,改业务代码不用重下依赖。

4. docker-compose 编排

version: "3.8"
services:
  gin-demo:
    build: .
    environment:
      - JWT_SECRET=${JWT_SECRET}
    ports:
      - "8080:8080"
    restart: unless-stopped

restart: unless-stopped 让进程意外退出自动拉起。密钥走宿主环境变量,compose 文件里不写明文。

5. nginx 反代

Gin 不直接暴露公网,前面挂 nginx 做 SSL 和转发:

server {
    listen 443 ssl;
    server_name api.demo.com;

    ssl_certificate     /etc/letsencrypt/live/api.demo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.demo.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

proxy_set_header 把真实客户端 IP 透给 Gin,否则 c.ClientIP() 拿到的是 nginx 的地址。证书用 Let's Encrypt 免费签发,90 天续期一次。

6. 健康检查

给编排和负载均衡一个探活 endpoint:

r.GET("/healthz", func(c *gin.Context) {
    c.JSON(200, gin.H{"status": "ok"})
})

k8s 的 readinessProbe、docker-compose 的 healthcheck 都打这个路径。服务依赖 MySQL 时,健康检查里顺手 db.Raw("SELECT 1") 探一下库,库挂了探针失败,流量不再打进来。

7. 上线检查清单

密钥全部走环境变量,配置文件不提交密钥。编译开 CGO_ENABLED=0。进程接 SIGTERM 做优雅停机。nginx 只暴露 443,Gin 端口不对外。健康检查 /healthz 已接。日志用结构化输出,方便后面接采集。

八篇走完,你已经有了一条从 go mod init 到生产部署的完整 Gin 链路。下一步可以补单元测试、接 Redis 缓存、做链路追踪,按业务需要往这套骨架上加就行。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/184