Skip to content

11.6 Docker 生产部署 ​

概述

项目已内置 Docker 化文件:docker/Dockerfile(多阶段静态编译)、docker/docker-compose.yml(后端 + MySQL + Redis 编排)、docker/.env.example(变量模板)、根目录 .dockerignore。开发环境快速体验见 2.7 Docker 快速体验,本节聚焦生产环境的加固与运维要点。

基线:内置文件的生产就绪点 ​

docker/Dockerfile 已包含的生产就绪设计(注释同步自源文件):

text
□ CGO_ENABLED=0 静态编译(sqlite 驱动为 modernc 系,无 CGO 依赖)
□ -trimpath -ldflags "-s -w":去除本地路径与调试符号,二进制更小、不泄露构建路径
□ 运行时资产四件独立层:manifest/config、manifest/data(ip2region.xdb)、
  resource(字体/swagger)、templates(生成器模板)——业务代码变动不破坏资产层缓存
□ 非 root 运行(USER app,uid 1000)
□ tzdata + Asia/Shanghai(日志与 gcron 定时任务依赖系统时区)
□ HEALTHCHECK 打 /api/health(公开白名单路径,不带 /v1)
□ 镜像内无源码、无密钥(仅二进制 + 资产)

docker/docker-compose.yml 已包含:

text
□ 敏感配置全部 GF_ 前缀环境变量注入,镜像内 config.yaml 保持默认
  (同一镜像可直接部署 dev/prod,无需为环境单独打镜像)
□ mysql/redis healthcheck + depends_on condition: service_healthy(后端等依赖就绪再启动)
□ upload/log 宿主卷持久化(../upload、../log,相对 compose 文件目录需 ../ 回项目根)
□ 定稿 SQL 挂 docker-entrypoint-initdb.d(仅空数据卷导入一次)
□ 日志驱动 json-file + max-size 500m + max-file 10 轮转
□ restart: always

生产环境必改项 ​

sh
cd docker
copy .env.example .env
bash
# docker/.env —— 生产清单
# JWT_SECRET 生成(≥32 字节密码学随机,各环境独立、勿复用):
#   Linux:  openssl rand -hex 32
#   Windows: [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
# 轮换此值后所有已签发令牌立即失效,用户需重新登录(安排发版窗口执行)
JWT_SECRET=<生成的随机串>            # 令牌签名密钥,生产必改
MYSQL_PASSWORD=<强密码>              # MySQL 密码
REDIS_PASSWORD=<强密码>              # Redis 密码

# 连云数据库/Redis(RDS 等)时同时改主机与端口,compose 自带的 mysql/redis
# 服务可保留(不依赖即可)或整段删除后去掉后端的 depends_on
MYSQL_HOST=rm-xxx.mysql.rds.aliyuncs.com
MYSQL_PORT=3306
MYSQL_USER=<云库账号>
REDIS_HOST=r-xxx.redis.rds.aliyuncs.com
REDIS_PORT=6379
# 容器内 127.0.0.1 指向容器自身,连宿主机请用 host.docker.internal

镜像内 manifest/config/config.yaml 另有两处默认值偏开发,compose 已用环境变量覆盖,若用 docker run 部署需自行带上:

sh
# 生产覆盖(compose 已内置,docker run 场景勿遗漏)
GF_CAPTCHA_BYPASSCODE=""        # 清空万能验证码(默认 618618 仅开发用)
GF_APP_ERRORMSG="false"         # 关闭响应中的详细错误堆栈

定稿 SQL 与数据卷

initdb.d 挂载仅在 MySQL 数据卷为空时执行一次。后续表结构变更需手动执行 SQL 或 docker volume rm 清卷重建——生产环境严禁清卷,变更走 11.3 数据库迁移 的手工变更流程。

云服务替代(推荐拓扑) ​

生产推荐 MySQL/Redis 用云服务,compose 只编排后端:

yaml
services:
  easygoadmin:
    image: easygoadmin:latest          # 或私有仓库地址
    restart: unless-stopped            # 比 always 更适合手工维护场景
    ports: ["8000:8000"]
    environment:
      GF_DATABASE_DEFAULT_LINK: "mysql:user:pass@tcp(rm-xxx.mysql.rds.aliyuncs.com:3306)/easygoadmin?charset=utf8mb4&parseTime=true&loc=Local"
      GF_REDIS_DEFAULT_ADDRESS: "r-xxx.redis.rds.aliyuncs.com:6379"
      GF_REDIS_DEFAULT_PASS: "<云 Redis 密码>"
      GF_FILE_UPLOADDIR: "/data/upload"
      GF_JWT_SECRET: "${JWT_SECRET}"
      GF_CAPTCHA_BYPASSCODE: ""
      GF_APP_ERRORMSG: "false"
    volumes:
      - /data/easygoadmin/upload:/data/upload   # 云盘/NFS 绝对路径
    mem_limit: 1g                                # 防 OOM 拖垮宿主机
    logging:
      driver: json-file
      options: { max-size: "50m", max-file: "3" }

前端 Nginx 托管与反代 ​

docker/ 只覆盖后端;前端 ui/ 独立构建后由 Nginx 托管:

nginx
server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # 前端路由回退
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 接口与静态上传反代后端
    location /api/    { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
    location /upload/ { proxy_pass http://backend:8000; }
    location /api.json { proxy_pass http://backend:8000; }
}

受信代理与真实 IP(CDN / Nginx / Docker)

后端取真实客户端 IP 的规则:TCP 直连方在 api.proxyIps(支持 CIDR)内才信任 X-Forwarded-For 首段 / X-Real-IP,否则直接记录直连方 IP。三层要点:

  1. Docker 端口映射模式下,容器内 TCP 对端恒为网桥网关(如 172.19.0.1)——CDN/宿主机 Nginx 的回源经 NAT 后都是它。配置默认已含 172.16.0.0/12(Docker 网桥默认网段);自定义 docker 网桥子网时按实际网段补充(生产用 GF_API_PROXYIPS 覆盖)。
  2. CDN 侧须在回源请求中携带 X-Forwarded-For: <真实客户端IP>,且采用覆盖而非追加模式——若客户端自带伪造 XFF 被保留在首段,取首段会误判。
  3. 宿主机 Nginx 反代场景:Nginx 配 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,并把 Nginx 网段加入 GF_API_PROXYIPS。

症状对照:登录/操作日志记成 172.19.0.1 → 第 1 层网段缺失;记成 CDN 回源 IP → 第 2/3 层 XFF 未传递。切勿配置 0.0.0.0/0(等于允许任意来源伪造 XFF)。

公开路径勿加 /v1

/api/login、/api/register、/api/captcha、/api/oauth2/token、/api/health 为网关免版本路径,反代与白名单三处须一致,Nginx 无需特殊处理(location /api/ 前缀已覆盖)。

数据持久化 ​

数据容器内位置持久化方式
上传文件/data/upload宿主目录卷 / NFS / 对象存储改造(多实例必须共享)
运行日志/opt/easygoadmin/log + stdoutstdout 走容器日志驱动轮转;文件卷可选
ip2region.xdbmanifest/data随镜像(只读),升级镜像即更新
数据库MySQL 容器 / 云 RDS云优先,定时备份见 11.9 备份与恢复
RedisRedis 容器 / 云 Redis缓存可丢;JWT 黑名单与在线会话会重置(用户需重新登录)

零停机发布 ​

text
单实例:docker compose up -d --build(滚动间隙数秒)
多实例:
  1. 起新容器(新版本)→ HEALTHCHECK 通过
  2. 摘旧容器流量(Nginx upstream / compose scale 滚动)
  3. 停旧容器
约束:
  - 定时任务经 utility/cache 的 job:lock:{id} 执行锁(Redis SET NX),多实例不双跑
  - 上传目录必须共享卷,否则文件访问 404

k8s 要点 ​

text
Deployment(replicas≥2)+ Service + Ingress(HTTPS)
ConfigMap 挂配置 / Secret 挂密钥(GF_ 覆盖,勿改镜像内 config.yaml)
PVC 挂 /data/upload(RWX 或对象存储改造)
liveness/readinessProbe → /api/health(Dockerfile 已内置 HEALTHCHECK,探针复用同路径)
HPA 按 CPU/内存(会话状态在 Redis,无状态可水平扩)

镜像运维 ​

text
□ 定期重建(基础镜像 CVE 修复)
□ 私有仓库 + 拉取审计;镜像打版本 tag,避免仅用 latest
□ 构建在项目根执行:docker build -f docker/Dockerfile -t easygoadmin:<tag> .
□ .dockerignore 已排除 ui/bin/log/upload/document/hack/.env,镜像不含源码与密钥
□ 容器内不使用代码生成器(产物写容器文件系统,重建即丢)

小蚂蚁云团队 · 提供技术支持