11.6 Docker 生产部署
概述
项目已内置 Docker 化文件:docker/Dockerfile(多阶段静态编译)、docker/docker-compose.yml(后端 + MySQL + Redis 编排)、docker/.env.example(变量模板)、根目录 .dockerignore。开发环境快速体验见 2.7 Docker 快速体验,本节聚焦生产环境的加固与运维要点。
基线:内置文件的生产就绪点
docker/Dockerfile 已包含的生产就绪设计(注释同步自源文件):
□ 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 已包含:
□ 敏感配置全部 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生产环境必改项
cd docker
copy .env.example .env# 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 部署需自行带上:
# 生产覆盖(compose 已内置,docker run 场景勿遗漏)
GF_CAPTCHA_BYPASSCODE="" # 清空万能验证码(默认 618618 仅开发用)
GF_APP_ERRORMSG="false" # 关闭响应中的详细错误堆栈定稿 SQL 与数据卷
initdb.d 挂载仅在 MySQL 数据卷为空时执行一次。后续表结构变更需手动执行 SQL 或 docker volume rm 清卷重建——生产环境严禁清卷,变更走 11.3 数据库迁移 的手工变更流程。
云服务替代(推荐拓扑)
生产推荐 MySQL/Redis 用云服务,compose 只编排后端:
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 托管:
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。三层要点:
- Docker 端口映射模式下,容器内 TCP 对端恒为网桥网关(如 172.19.0.1)——CDN/宿主机 Nginx 的回源经 NAT 后都是它。配置默认已含
172.16.0.0/12(Docker 网桥默认网段);自定义 docker 网桥子网时按实际网段补充(生产用GF_API_PROXYIPS覆盖)。 - CDN 侧须在回源请求中携带
X-Forwarded-For: <真实客户端IP>,且采用覆盖而非追加模式——若客户端自带伪造 XFF 被保留在首段,取首段会误判。 - 宿主机 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 + stdout | stdout 走容器日志驱动轮转;文件卷可选 |
| ip2region.xdb | manifest/data | 随镜像(只读),升级镜像即更新 |
| 数据库 | MySQL 容器 / 云 RDS | 云优先,定时备份见 11.9 备份与恢复 |
| Redis | Redis 容器 / 云 Redis | 缓存可丢;JWT 黑名单与在线会话会重置(用户需重新登录) |
零停机发布
单实例:docker compose up -d --build(滚动间隙数秒)
多实例:
1. 起新容器(新版本)→ HEALTHCHECK 通过
2. 摘旧容器流量(Nginx upstream / compose scale 滚动)
3. 停旧容器
约束:
- 定时任务经 utility/cache 的 job:lock:{id} 执行锁(Redis SET NX),多实例不双跑
- 上传目录必须共享卷,否则文件访问 404k8s 要点
Deployment(replicas≥2)+ Service + Ingress(HTTPS)
ConfigMap 挂配置 / Secret 挂密钥(GF_ 覆盖,勿改镜像内 config.yaml)
PVC 挂 /data/upload(RWX 或对象存储改造)
liveness/readinessProbe → /api/health(Dockerfile 已内置 HEALTHCHECK,探针复用同路径)
HPA 按 CPU/内存(会话状态在 Redis,无状态可水平扩)镜像运维
□ 定期重建(基础镜像 CVE 修复)
□ 私有仓库 + 拉取审计;镜像打版本 tag,避免仅用 latest
□ 构建在项目根执行:docker build -f docker/Dockerfile -t easygoadmin:<tag> .
□ .dockerignore 已排除 ui/bin/log/upload/document/hack/.env,镜像不含源码与密钥
□ 容器内不使用代码生成器(产物写容器文件系统,重建即丢)