# 11.6 Docker 生产部署 :::tip 概述 项目已内置 Docker 化文件:`docker/Dockerfile`(多阶段静态编译)、`docker/docker-compose.yml`(后端 + MySQL + Redis 编排)、`docker/.env.example`(变量模板)、根目录 `.dockerignore`。开发环境快速体验见 [2.7 Docker 快速体验](../quickstart/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" # 关闭响应中的详细错误堆栈 ``` ::: warning 定稿 SQL 与数据卷 initdb.d 挂载**仅在 MySQL 数据卷为空时执行一次**。后续表结构变更需手动执行 SQL 或 `docker volume rm` 清卷重建——**生产环境严禁清卷**,变更走 [11.3 数据库迁移](./db-migrate) 的手工变更流程。 ::: ## 云服务替代(推荐拓扑) 生产推荐 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; } } ``` ::: tip 受信代理与真实 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)。 ::: ::: tip 公开路径勿加 /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 备份与恢复](./backup) | | Redis | Redis 容器 / 云 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: . □ .dockerignore 已排除 ui/bin/log/upload/document/hack/.env,镜像不含源码与密钥 □ 容器内不使用代码生成器(产物写容器文件系统,重建即丢) ```