Files
easygoadmin-wiki/EasyGoAdmin_GoFrame_EleVue/zh/deploy/docker.md
T
2026-10-20 21:19:44 +08:00

178 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<tag> .
□ .dockerignore 已排除 ui/bin/log/upload/document/hack/.env,镜像不含源码与密钥
□ 容器内不使用代码生成器(产物写容器文件系统,重建即丢)
```