Compare commits
20 Commits
536d520744
...
b11360015d
| Author | SHA1 | Date | |
|---|---|---|---|
| b11360015d | |||
| 3b2856f388 | |||
| 9d9fbc62f5 | |||
| 807f4c2448 | |||
| da061d35db | |||
| d0544c9bd2 | |||
| 3f5eacae60 | |||
| b25d0798f6 | |||
| 6b1d23c7d7 | |||
| 6951b8049f | |||
| da0424a268 | |||
| 569263dddb | |||
| 646929ddff | |||
| 0a7c5ee617 | |||
| 93400b7793 | |||
| a5aa5f81ad | |||
| 86c865e710 | |||
| 1bd69077a2 | |||
| f37ebf8621 | |||
| e42dc77127 |
@@ -69,6 +69,7 @@ web/uploads/
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
web/data/1panel-hosts.json
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
@@ -10,15 +10,19 @@
|
||||
| 安装向导 | `https://你的域名/app/install.html` |
|
||||
| 文档索引 | [docs/README.md](docs/README.md) |
|
||||
|
||||
生产 Docker 栈 **仅含 Nexus 容器**;MySQL / Redis 在宿主机或 1Panel 自行安装。
|
||||
|
||||
---
|
||||
|
||||
## 一键安装(公共仓库,推荐)
|
||||
## 一键命令速查(root · 公共仓库 · 无需令牌)
|
||||
|
||||
在 **root** 登录的 Linux 服务器上执行(仿 [1Panel](https://1panel.cn/) 风格,无需 Gitea Token):
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
| 场景 | 一条命令 |
|
||||
|------|----------|
|
||||
| **仅装 Nexus**(1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| **重装服务器**(1Panel + Docker + Nexus) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" \| bash` |
|
||||
| **一键更新**(pull + 重建镜像) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" \| bash` |
|
||||
| **强制无缓存重建**(entrypoint 变更后) | `curl -fsSL ".../deploy/update.sh" \| bash -s -- --no-cache` |
|
||||
| **已 clone** | `cd /opt/nexus && bash deploy/update.sh` |
|
||||
|
||||
指定资源档位(`1c4g` / `2c8g` 默认 / `4c16g`):
|
||||
|
||||
@@ -26,62 +30,41 @@ curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-in
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
可选:先下载再执行(便于审计脚本内容):
|
||||
**raw 地址**:`http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
bash /tmp/quick-install.sh --profile 2c8g
|
||||
```
|
||||
|
||||
**raw 地址说明**(匿名可访问,须返回 `#!/` 而非 HTML 404):
|
||||
|
||||
- `http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
|
||||
- 等价:`http://66.154.115.8:3000/raw/main/deploy/<脚本名>`
|
||||
|
||||
安装完成后在浏览器打开安装向导,完成 `.env` / MySQL / 管理员初始化。
|
||||
|
||||
**每台新服务器的密钥自动不同**:安装脚本生成 `MYSQL_*` 与 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` 写入 `docker/.env.prod`,备份在 `/root/.nexus-install-secrets-*.env`。勿把生产密钥放进 `nexus-1panel.secrets.sh`(仅保留 `NEXUS_GITEA_TOKEN`)。
|
||||
安装完成后浏览器打开安装向导,完成 MySQL(自建)/ Redis(自建)/ 管理员初始化。
|
||||
|
||||
---
|
||||
|
||||
## 已 clone 到本机(跳过再次拉仓)
|
||||
## NX 统一运维菜单
|
||||
|
||||
装好后在服务器任意目录输入 **`nx`**(`install` / `update` / 首次运行 `nx` 会自动注册 `/usr/local/bin/nx`):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
nx # 统一菜单:安装 + 一键更新 + 重启 + 日志 + 备份
|
||||
nx update # 非交互一键更新
|
||||
nx update --no-cache
|
||||
nx install-fresh # 全新安装
|
||||
nx health # 健康检查
|
||||
```
|
||||
|
||||
或交互菜单:
|
||||
| 菜单项 | 作用 |
|
||||
|--------|------|
|
||||
| `[4] 一键更新` | pull + 重建 Docker 镜像(等同 `deploy/update.sh`) |
|
||||
| `[5] 重启 Nexus` | 重启 Python 应用 |
|
||||
| `[6] 日志` | 跟踪容器日志 |
|
||||
| `[1][2]` | 安装 / 全新安装向导 |
|
||||
|
||||
```bash
|
||||
nx
|
||||
# 选「全新安装」或子命令: nx install-fresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NX 运维菜单(装好后)
|
||||
|
||||
安装脚本会向 `PATH` 注册:
|
||||
全局命令(符号链接):
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `nx` | 交互主菜单(安装 / 升级 / 健康检查) |
|
||||
| `nx god` | **上帝菜单**:重启 Python、全栈重启、日志、升级、备份等 |
|
||||
| `nx` | 统一运维菜单 |
|
||||
| `nexus-update` | 等同 `deploy/update.sh` |
|
||||
| `nexus-fresh` | 等同 `install-nexus-fresh.sh` |
|
||||
| `nexus-install` | 快捷安装入口 |
|
||||
| `nexus-install` | 等同 `quick-install.sh` |
|
||||
|
||||
常用子命令:
|
||||
|
||||
```bash
|
||||
nx # 主菜单
|
||||
nx god # 上帝运维菜单
|
||||
nx install-fresh # 全新空库安装 → /app/install.html
|
||||
nx install-auto # 非交互安装(需已配置密钥,默认 2c8g)
|
||||
nx health # 健康检查 / 端口预检
|
||||
```
|
||||
|
||||
底层仍可直接调用:`bash deploy/nexus-1panel.sh install|upgrade|check|ports`
|
||||
> `nx god` 已合并进 `nx` 主菜单,仍可作为兼容别名使用。
|
||||
|
||||
---
|
||||
|
||||
@@ -89,30 +72,41 @@ nx health # 健康检查 / 端口预检
|
||||
|
||||
### 1Panel + Docker 生产(主路径)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库 curl 入口**;clone + 调用全新安装 |
|
||||
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新安装(空库 → 安装向导);支持 `--skip-clone`、`--profile` |
|
||||
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 迁机 / 升级 / 检查 / 端口预检;可被 `nx` 调用 |
|
||||
| [nx](deploy/nx) | 交互主菜单 + 上帝菜单 |
|
||||
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 转调 `nx` 的 Docker 安装包装 |
|
||||
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nx` 的升级包装 |
|
||||
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 从 Gitea API/raw 下载安装脚本(私有库需 Token) |
|
||||
| 脚本 | 用途 | curl 入口 |
|
||||
|------|------|-----------|
|
||||
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库安装入口** | ✅ 上表 |
|
||||
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 1Panel + Docker + Nexus 全链 | ✅ 上表 |
|
||||
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新空库 → 安装向导 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| [update.sh](deploy/update.sh) | **一键更新**(pull + 重建) | ✅ 上表 |
|
||||
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nexus-1panel.sh upgrade` | — |
|
||||
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 安装 / 升级 / check / ports 底层 | — |
|
||||
| [nx](deploy/nx) | **统一交互菜单** | `nx` |
|
||||
| [uninstall-mysql-compose.sh](deploy/uninstall-mysql-compose.sh) | 卸载遗留 Compose MySQL 容器 | — |
|
||||
| [uninstall-redis-compose.sh](deploy/uninstall-redis-compose.sh) | 卸载遗留 Compose Redis 容器 | — |
|
||||
| [sync-install-wizard-to-container.sh](deploy/sync-install-wizard-to-container.sh) | 热同步 install.html 到容器 | — |
|
||||
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 下载安装脚本(私有库可选 Token) | — |
|
||||
|
||||
详细说明:[deploy/README-1panel.md](deploy/README-1panel.md)
|
||||
|
||||
### 更新选项
|
||||
|
||||
```bash
|
||||
bash deploy/update.sh # 默认:Git 拉取(若有)+ 重建容器
|
||||
bash deploy/update.sh --check # 仅查 Git
|
||||
bash deploy/update.sh --no-cache # 无缓存重建镜像
|
||||
bash deploy/update.sh --no-backup # 跳过 MySQL 备份
|
||||
```
|
||||
|
||||
### Docker Compose 与档位
|
||||
|
||||
| 路径 | 用途 |
|
||||
|------|------|
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(变量化 CPU/内存) |
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(**仅 nexus 服务**) |
|
||||
| [docker/profiles/1c4g.env](docker/profiles/1c4g.env) | 1 核 4G |
|
||||
| [docker/profiles/2c8g.env](docker/profiles/2c8g.env) | 2 核 8G(默认) |
|
||||
| [docker/profiles/4c16g.env](docker/profiles/4c16g.env) | 4 核 16G |
|
||||
| [docker/README.md](docker/README.md) | 镜像与本地开发说明 |
|
||||
|
||||
安装参数:`--profile 1c4g|2c8g|4c16g`
|
||||
|
||||
### 经典 SSH / Supervisor 部署(宝塔 / 裸机)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
@@ -122,20 +116,10 @@ nx health # 健康检查 / 端口预检
|
||||
| [deploy-on-server.sh](deploy/deploy-on-server.sh) | 服务器侧部署 |
|
||||
| [deploy-production.sh](deploy/deploy-production.sh) | 生产发布流程 |
|
||||
| [deploy-frontend.sh](deploy/deploy-frontend.sh) | 构建 Vue 前端并打包到 `web/app/` |
|
||||
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控**(changelog / 审计 / 测试 / ruff / import / bandit / review) |
|
||||
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查,`/health` 失败重启 |
|
||||
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份(30 天保留) |
|
||||
| [run_test_api_on_server.sh](deploy/run_test_api_on_server.sh) | 服务器上跑 `test_api` |
|
||||
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控** |
|
||||
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查 |
|
||||
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份 |
|
||||
| [uninstall.sh](deploy/uninstall.sh) | 卸载 |
|
||||
| [check_shell_eol.sh](deploy/check_shell_eol.sh) | Shell 脚本 EOL 检查 |
|
||||
|
||||
### 本地与验收
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [scripts/local_verify.sh](scripts/local_verify.sh) | L2b 本地验证(ruff / import / 前端 typecheck 等) |
|
||||
| [scripts/run_nexus_acceptance.py](scripts/run_nexus_acceptance.py) | L4 验收 |
|
||||
| [verify.py](verify.py) | 快速验证入口 |
|
||||
|
||||
---
|
||||
|
||||
@@ -143,10 +127,12 @@ nx health # 健康检查 / 端口预检
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| **新 VPS、空 MySQL** | `curl \| bash` 或 `install-nexus-fresh.sh` → 浏览器走完安装向导 |
|
||||
| **已有数据、换机** | `nexus-1panel.sh install`(带备份/恢复逻辑)或 `nx` 迁机流程 |
|
||||
| **仅升级代码/镜像** | `nx` → 升级,或 `upgrade-1panel-docker.sh` / `nexus-1panel.sh upgrade` |
|
||||
| **不必重装整台 VPS** | 在 `/opt/nexus` 内 `git pull` + `nx god` 重启即可 |
|
||||
| **新 VPS、空库** | `curl \| quick-install.sh` → 1Panel 自建 MySQL/Redis → `/app/install.html` |
|
||||
| **已有数据、换机** | `nexus-1panel.sh install --from-env` 或 `nx` → 安装向导 |
|
||||
| **仅升级代码/镜像** | `nx update` 或 `bash deploy/update.sh` |
|
||||
| **不必重装 VPS** | `cd /opt/nexus && nx update` |
|
||||
|
||||
每台新服务器自动生成唯一 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(写入 `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`)。MySQL 密码由自建库时自行设定。
|
||||
|
||||
---
|
||||
|
||||
@@ -157,32 +143,25 @@ Nexus/
|
||||
├── server/ # FastAPI 后端(Clean Architecture)
|
||||
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
|
||||
├── web/app/ # 静态资源挂载点(构建产物 gitignore)
|
||||
├── deploy/ # 安装、门控、健康检查、运维脚本
|
||||
├── deploy/ # 安装、更新、门控、nx 菜单
|
||||
├── docker/ # Compose、镜像、profiles
|
||||
├── docs/ # 设计 / changelog / 审计 / 报告
|
||||
├── tests/ # API 与集成测试
|
||||
└── standards/ # 开发与逐行审计标准
|
||||
```
|
||||
|
||||
启动模式:
|
||||
|
||||
- **无 `.env`**:仅 `/api/install/` + 安装页(安装模式)
|
||||
- **有 `.env`**:完整业务 API + 后台任务
|
||||
|
||||
---
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 后端(需 .env)
|
||||
pip install -r requirements.txt
|
||||
uvicorn server.main:app --reload --port 8600
|
||||
|
||||
# 前端
|
||||
cd frontend && npm run dev # http://localhost:3000/app/
|
||||
```
|
||||
|
||||
Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
|
||||
Linux 路径:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
|
||||
|
||||
---
|
||||
|
||||
@@ -190,12 +169,11 @@ Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/lin
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [docs/README.md](docs/README.md) | 文档总索引(SSOT) |
|
||||
| [docs/README.md](docs/README.md) | 文档总索引 |
|
||||
| [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) | 功能 / API / 14 页 |
|
||||
| [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md) | 当前会话接续 |
|
||||
| [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md) | Agent 薄入口 |
|
||||
|
||||
变更记录:`docs/changelog/` · 门控审计:`docs/audit/`
|
||||
变更:`docs/changelog/` · 门控:`docs/audit/`
|
||||
|
||||
---
|
||||
|
||||
@@ -203,10 +181,9 @@ Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/lin
|
||||
|
||||
| 用途 | 地址 |
|
||||
|------|------|
|
||||
| 仓库首页(产品说明 / README) | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git 克隆 / push | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
| 仓库首页 | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
|
||||
- 分支:`main`
|
||||
- 提交前请通过 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
分支 `main` · 提交前跑 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
|
||||
**注意**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md` 含敏感信息,**勿提交到 Git**。生产密钥仅保存在服务器与本机私密文档中。
|
||||
**勿提交**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md`
|
||||
|
||||
+169
-55
@@ -1,101 +1,215 @@
|
||||
# 1Panel + Docker 一键部署
|
||||
|
||||
## 重装服务器(推荐:1Panel + Docker + Nexus 一条链)
|
||||
> 完整知识文档:[docs/project/nexus-1panel-operations-knowledge.md](../docs/project/nexus-1panel-operations-knowledge.md)
|
||||
|
||||
## 脚本速查(公共仓库 · 无需令牌)
|
||||
|
||||
| 脚本 | 用途 | 一条命令 |
|
||||
|------|------|----------|
|
||||
| `quick-install.sh` | 仅装 Nexus(1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| `install-1panel-docker.sh` | 重装服务器全链 | `curl -fsSL ".../install-1panel-docker.sh" \| bash` |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| **`update.sh`** | **一键更新** | `curl -fsSL ".../update.sh" \| bash` |
|
||||
| `nexus-1panel.sh` | 底层 install / upgrade | `bash deploy/nexus-1panel.sh upgrade` |
|
||||
| `fix-1panel-mysql-grant.sh` | 修复 1045 nexus@Docker | `bash deploy/fix-1panel-mysql-grant.sh` |
|
||||
| `test-1panel-redis.sh` | Redis 认证诊断 | `REDIS_PASS='...' bash deploy/test-1panel-redis.sh` |
|
||||
| **`nx`** | **统一运维菜单** | `nx` |
|
||||
|
||||
档位:`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
|
||||
---
|
||||
|
||||
## 重装服务器(1Panel + Docker + Nexus)
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
```
|
||||
|
||||
脚本会:未装 1Panel 时提示跑官方安装 → 检测 Docker Compose → 拉代码并启动 Nexus 容器栈。
|
||||
|
||||
4 核 16G:
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
已装 1Panel,只装 Nexus:
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
```
|
||||
|
||||
## 仅 Nexus Docker(1Panel 已就绪)
|
||||
## 仅 Nexus Docker
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
|
||||
安装完成后浏览器打开 `https://api.synaglobal.vip/app/install.html` 完成向导。
|
||||
安装完成后:`https://api.synaglobal.vip/app/install.html`
|
||||
|
||||
## Redis(自行安装,脚本不附带)
|
||||
---
|
||||
|
||||
Docker 栈仅含 **MySQL + Nexus**。Redis 请在宿主机或 1Panel 应用商店单独安装,监听 `127.0.0.1:6379`(或改 `docker/.env.prod` 的 `NEXUS_REDIS_URL`)。
|
||||
|
||||
安装向导步骤 3:Docker 部署时 Redis 主机填 **`host.docker.internal`**,端口 `6379`。
|
||||
|
||||
## 1Panel 反代(必须在面板 UI 操作)
|
||||
|
||||
**禁止** Agent/脚本直接 `mkdir` 或 `tee` 到 `/opt/1panel/apps/openresty/` — 会破坏 bind mount。
|
||||
|
||||
1. 1Panel → 应用商店 → 安装 **OpenResty**
|
||||
2. 网站 → 创建站点 → 反向代理 `http://127.0.0.1:8600`
|
||||
3. 自定义配置参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
## 已 clone 到 /opt/nexus
|
||||
## 一键更新(日常)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
cd /opt/nexus && bash deploy/update.sh
|
||||
# 或
|
||||
nx update
|
||||
# 或菜单
|
||||
nx # → [4] 一键更新
|
||||
```
|
||||
|
||||
## NX 运维菜单
|
||||
|
||||
```bash
|
||||
nx # 主菜单
|
||||
nx god # 上帝菜单(重启 Python / 升级 / 日志)
|
||||
```
|
||||
|
||||
装好后会注册:`nexus-install` · `nx` · `nexus-fresh`
|
||||
|
||||
## 脚本说明
|
||||
|
||||
| 文件 | 用途 |
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `install-1panel-docker.sh` | **重装服务器**一键链(1Panel + Docker + Nexus) |
|
||||
| `quick-install.sh` | 仅 Nexus Docker(1Panel 已装好) |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装(被上面两个脚本调用) |
|
||||
| `nexus-1panel.sh` | 迁机 / 升级底层 |
|
||||
| `nx` | 交互菜单 |
|
||||
| `--check` | 仅查 Git,不重建 |
|
||||
| `--no-cache` | 无缓存重建镜像(entrypoint 变更后必用) |
|
||||
| `--no-backup` | 跳过 MySQL 备份 |
|
||||
| `--prune` | 清理悬空镜像 |
|
||||
|
||||
## 资源档位
|
||||
远程:
|
||||
|
||||
`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
curl -fsSL ".../update.sh" | bash -s -- --no-cache
|
||||
```
|
||||
|
||||
## 密钥(每台新服务器唯一)
|
||||
---
|
||||
|
||||
全新安装(`install-nexus-fresh.sh` / `--fresh`)会调用 `scripts/generate_nexus_secrets.py` 自动生成:
|
||||
## NX 全局命令
|
||||
|
||||
- `MYSQL_ROOT_PASSWORD` / `MYSQL_PASSWORD`
|
||||
- `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(与安装向导算法一致)
|
||||
安装 / 升级 / 首次运行 `nx` 会自动注册到 `/usr/local/bin`:
|
||||
|
||||
写入 `docker/.env.prod`,备份到 `/root/.nexus-install-secrets-*.env`(chmod 600)。
|
||||
```bash
|
||||
# 手动注册(任意目录可用 nx)
|
||||
NEXUS_ROOT=/opt/nexus bash /opt/nexus/deploy/install-nx-cli.sh
|
||||
|
||||
nx # 统一运维菜单
|
||||
nexus-update # 等同 update.sh
|
||||
nexus-fresh # 等同 install-nexus-fresh.sh
|
||||
nexus-install # 等同 quick-install.sh
|
||||
```
|
||||
|
||||
| 菜单 | 功能 |
|
||||
|------|------|
|
||||
| `[1]` | 安装向导(档位 / 迁机 / skip-clone) |
|
||||
| `[2]` | 全新安装 → `/app/install.html` |
|
||||
| `[4]` | **一键更新** |
|
||||
| `[5]` | 重启 Nexus |
|
||||
| `[6]` | 日志 |
|
||||
|
||||
---
|
||||
|
||||
## MySQL / Redis(1Panel 应用商店)
|
||||
|
||||
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
|
||||
|
||||
### 1Panel 官方互联方式(必读)
|
||||
|
||||
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`。
|
||||
|
||||
| 场景 | 正确主机 |
|
||||
|------|----------|
|
||||
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
|
||||
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
|
||||
| OpenResty → 应用 | `127.0.0.1:端口`(OpenResty 为 host 网络) |
|
||||
|
||||
`nx update` / `nexus-1panel.sh` 会:
|
||||
|
||||
1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`)
|
||||
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`)
|
||||
3. 安装向导步骤 3 **预填 MySQL/Redis 容器名**
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`)
|
||||
|
||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||
|
||||
TCP 已通,但 MySQL 用户多为 **`nexus@localhost`**,Docker 内 Nexus 从 **172.x** 连接会被拒绝。
|
||||
|
||||
在服务器执行(按提示输入 root 密码与 nexus 密码):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
或一次性:
|
||||
|
||||
```bash
|
||||
MYSQL_ROOT_PASSWORD='1Panel里MySQL的root密码' \
|
||||
NEXUS_DB_PASSWORD='向导里要填的nexus密码' \
|
||||
bash /opt/nexus/deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
然后安装向导步骤 3:**库名/用户 `nexus`**,主机为 `1Panel-mysql-xxxx`,密码与上面一致。
|
||||
|
||||
若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
|
||||
|
||||
卸载旧 Compose 容器:
|
||||
|
||||
```bash
|
||||
bash deploy/uninstall-mysql-compose.sh
|
||||
bash deploy/uninstall-redis-compose.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1Panel 反代
|
||||
|
||||
**禁止**脚本改 `/opt/1panel/apps/openresty/`。
|
||||
|
||||
1. 应用商店 → **OpenResty**
|
||||
2. 网站 → 反代 `http://127.0.0.1:8600`
|
||||
3. 参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
---
|
||||
|
||||
## 密钥
|
||||
|
||||
新机自动生成 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` → `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`。
|
||||
|
||||
MySQL 密码在 1Panel 自建库时设定,向导步骤 3 填写。
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 新机(推荐) | `curl \| bash` 或 `install-nexus-fresh.sh` |
|
||||
| 迁机保留旧密钥 | `nexus-1panel.sh install --from-env /path/to/old/.env` |
|
||||
| 显式复用 secrets 文件 | `nexus-1panel.sh install --reuse-secrets`(**勿用于新机**) |
|
||||
| 新机 | `curl \| quick-install.sh` |
|
||||
| 迁机 | `nexus-1panel.sh install --from-env /path/to/.env` |
|
||||
| 更新 | `update.sh` 或 `nx update` |
|
||||
|
||||
**Gitea 令牌**:公共仓库 `curl | bash` / `git clone` **不需要**配置。仅私人仓库时可选 `/root/.nexus-deploy.env` 或 `nexus-1panel.sh init-token`。
|
||||
**Git**:公开仓库 `clone` / `pull` / `curl | bash` 无需令牌。
|
||||
|
||||
## 安装向导 `/app/install.html`
|
||||
---
|
||||
|
||||
生产镜像 `Dockerfile.prod` 已内置 `install.html` + `vendor`(Alpine/Tailwind)。升级后若 404:
|
||||
## 安装向导验收(服务器执行)
|
||||
|
||||
在 **1Panel 终端**(root)一键检查网络、容器名预填、本地 `/health` 与 `/api/install/env-check`:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/verify-1panel-install-wizard.sh
|
||||
```
|
||||
|
||||
通过后再浏览器打开 `https://你的域名/app/install.html`。常见修复:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
外网 HTTPS 仍失败但脚本本地检查全绿 → 在 1Panel **网站**配置反代 `http://127.0.0.1:8600`(见 `deploy/1panel/openresty-nexus.conf.example`)。
|
||||
|
||||
## 安装向导热修复
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/nexus-1panel.sh upgrade # 会 --build 重建镜像
|
||||
# 或仅同步到当前容器(旧镜像兜底):
|
||||
bash deploy/update.sh --no-cache
|
||||
# 或仅同步静态:
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
|
||||
# See: https://github.com/1Panel-dev/1Panel/issues/11676
|
||||
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
network_container_names() {
|
||||
docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
|
||||
| tr ' ' '\n' | sed '/^$/d'
|
||||
}
|
||||
|
||||
pick_service_container() {
|
||||
local kind="$1"
|
||||
local names line
|
||||
names="$(network_container_names)"
|
||||
if [[ -z "$names" ]]; then
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true
|
||||
return
|
||||
fi
|
||||
line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)"
|
||||
if [[ -n "$line" ]]; then
|
||||
echo "$line"
|
||||
return
|
||||
fi
|
||||
echo "$names" | grep -i "$kind" | head -1 || true
|
||||
}
|
||||
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
echo "1panel-network not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_HOST="$(pick_service_container mysql)"
|
||||
REDIS_HOST="$(pick_service_container redis)"
|
||||
|
||||
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
|
||||
echo "No MySQL/Redis containers on 1panel-network" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
|
||||
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grant nexus@'%' on 1Panel App Store MySQL for Docker (1panel-network) install wizard.
|
||||
# Fixes: Access denied for user 'nexus'@'172.18.x.x' (1045)
|
||||
#
|
||||
# Usage:
|
||||
# MYSQL_ROOT_PASSWORD='root密码' NEXUS_DB_PASSWORD='nexus密码' bash deploy/fix-1panel-mysql-grant.sh
|
||||
# bash deploy/fix-1panel-mysql-grant.sh # prompts
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_DB_NAME="${NEXUS_DB_NAME:-nexus}"
|
||||
NEXUS_DB_USER="${NEXUS_DB_USER:-nexus}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||
|
||||
pick_mysql_container() {
|
||||
local name
|
||||
if [[ -n "${MYSQL_CONTAINER:-}" ]]; then
|
||||
echo "$MYSQL_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
name="$(docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' \
|
||||
| tr ' ' '\n' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
fi
|
||||
name="$(docker ps --format '{{.Names}}' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
error "未找到 1Panel MySQL 容器(可设 MYSQL_CONTAINER=名称)"
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var="$1" prompt="$2"
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -r -s -p "$prompt: " "$var"
|
||||
echo ""
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
error "$var 不能为空"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ "${EUID:-$(id -u)}" -eq 0 ]] || error "请用 root 运行"
|
||||
|
||||
local mysql_c
|
||||
mysql_c="$(pick_mysql_container)"
|
||||
info "MySQL 容器: $mysql_c"
|
||||
|
||||
prompt_secret MYSQL_ROOT_PASSWORD "1Panel MySQL root 密码"
|
||||
prompt_secret NEXUS_DB_PASSWORD "nexus 用户密码(与安装向导步骤 3 一致)"
|
||||
|
||||
info "创建库/用户并授权 ${NEXUS_DB_USER}@'%' → ${NEXUS_DB_NAME}.* ..."
|
||||
|
||||
docker exec -i "$mysql_c" mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`${NEXUS_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
ALTER USER '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
GRANT ALL PRIVILEGES ON \`${NEXUS_DB_NAME}\`.* TO '${NEXUS_DB_USER}'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
SELECT user, host FROM mysql.user WHERE user = '${NEXUS_DB_USER}';
|
||||
SQL
|
||||
|
||||
info "完成。请在安装向导步骤 3 使用: 库/用户=${NEXUS_DB_NAME}/${NEXUS_DB_USER},密码与上面一致"
|
||||
info "若已改 install.py,执行: bash deploy/sync-install-wizard-to-container.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Regular → Executable
Regular → Executable
+6
-18
@@ -66,7 +66,7 @@ Nexus 6.0 全新安装(1Panel + Docker)
|
||||
-h, --help 显示帮助
|
||||
|
||||
安装后请用浏览器打开: https://域名/app/install.html
|
||||
root 下全局命令: nexus-fresh | nx | nx god
|
||||
root 下全局命令: nexus-fresh | nexus-update | nx
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -120,20 +120,8 @@ load_credentials() {
|
||||
|
||||
install_root_commands() {
|
||||
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
|
||||
chmod +x "${NEXUS_ROOT}/deploy/nx" \
|
||||
"${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" \
|
||||
"${NEXUS_ROOT}/deploy/nexus-1panel.sh" \
|
||||
"${NEXUS_ROOT}/deploy/sync-install-wizard-to-container.sh" 2>/dev/null || true
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nx" /usr/local/bin/nx
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nexus-1panel.sh" /usr/local/bin/nexus-1panel
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" /usr/local/bin/nexus-fresh
|
||||
ln -sf "${NEXUS_ROOT}/deploy/quick-install.sh" /usr/local/bin/nexus-install
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-1panel-docker.sh" /usr/local/bin/nexus-1panel-docker 2>/dev/null || true
|
||||
info "已注册全局命令(root 直接输入即可):"
|
||||
info " nexus-1panel-docker — 重装服务器(1Panel + Docker + Nexus)"
|
||||
info " nexus-install — 仅 Nexus Docker(curl 入口)"
|
||||
info " nx — 主菜单 / 上帝菜单"
|
||||
info " nexus-fresh — 全新安装脚本"
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "${NEXUS_ROOT}/deploy/install-nx-cli.sh" || true
|
||||
info "全局命令: nx · nexus-update · nexus-fresh · nexus-install · nexus-1panel"
|
||||
}
|
||||
|
||||
phase_preflight() {
|
||||
@@ -286,9 +274,9 @@ phase_post() {
|
||||
echo -e "${GREEN}║${NC} 目标: http://127.0.0.1:${port} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维(上帝菜单)${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx god # 重启 Python / 升级 / 日志 / 备份 ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 主菜单 ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 统一菜单(一键更新 / 重启 / 日志) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nexus-update # 等同 deploy/update.sh ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 注册 Nexus 全局 CLI(/usr/local/bin)— install / upgrade / nx 均会调用
|
||||
#
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/install-nx-cli.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
BIN_DIR="${NEXUS_BIN_DIR:-/usr/local/bin}"
|
||||
DEPLOY="${NEXUS_ROOT}/deploy"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ ! -d "$DEPLOY" ]]; then
|
||||
error "未找到 $DEPLOY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
warn "非 root:无法写入 $BIN_DIR,请使用 root 执行或: sudo bash $DEPLOY/install-nx-cli.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
chmod +x \
|
||||
"$DEPLOY/nx" \
|
||||
"$DEPLOY/update.sh" \
|
||||
"$DEPLOY/nexus-1panel.sh" \
|
||||
"$DEPLOY/install-nexus-fresh.sh" \
|
||||
"$DEPLOY/quick-install.sh" \
|
||||
"$DEPLOY/install-1panel-docker.sh" \
|
||||
"$DEPLOY/upgrade-1panel-docker.sh" \
|
||||
"$DEPLOY/sync-install-wizard-to-container.sh" \
|
||||
"$DEPLOY/uninstall-mysql-compose.sh" \
|
||||
"$DEPLOY/uninstall-redis-compose.sh" \
|
||||
"$DEPLOY/install-nx-cli.sh" \
|
||||
2>/dev/null || true
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
link_cli() {
|
||||
local src="$1" name="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
warn "跳过 $name(缺少 $src)"
|
||||
return 0
|
||||
fi
|
||||
ln -sf "$src" "${BIN_DIR}/${name}"
|
||||
}
|
||||
|
||||
link_cli "$DEPLOY/nx" nx
|
||||
link_cli "$DEPLOY/update.sh" nexus-update
|
||||
link_cli "$DEPLOY/install-nexus-fresh.sh" nexus-fresh
|
||||
link_cli "$DEPLOY/quick-install.sh" nexus-install
|
||||
link_cli "$DEPLOY/nexus-1panel.sh" nexus-1panel
|
||||
link_cli "$DEPLOY/install-1panel-docker.sh" nexus-1panel-docker
|
||||
|
||||
if command -v nx >/dev/null 2>&1; then
|
||||
info "全局命令已就绪: nx → $(readlink -f "$(command -v nx)" 2>/dev/null || command -v nx)"
|
||||
else
|
||||
error "nx 未出现在 PATH 中,请确认 $BIN_DIR 在 PATH 内"
|
||||
exit 1
|
||||
fi
|
||||
Regular → Executable
+174
-22
@@ -34,6 +34,7 @@ NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
|
||||
COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
ENV_PROD="docker/.env.prod"
|
||||
HOST_PROFILE_FILE="docker/.host-profile"
|
||||
|
||||
@@ -54,6 +55,7 @@ REUSE_SECRETS=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
NO_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
NO_CACHE=false
|
||||
CHECK_ONLY=false
|
||||
DRY_RUN=false
|
||||
CUSTOM_CPUS=""
|
||||
@@ -81,7 +83,7 @@ Nexus 1Panel 一键脚本
|
||||
--domain NAME 覆盖域名
|
||||
|
||||
升级选项:
|
||||
--no-backup --prune --dry-run
|
||||
--no-backup --prune --no-cache --dry-run --check(仅查 Git,不重建)
|
||||
|
||||
Git: 公共仓库匿名 clone(无需令牌);私人仓库见 init-token
|
||||
EOF
|
||||
@@ -185,6 +187,84 @@ apply_pool_to_env_prod() {
|
||||
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
||||
}
|
||||
|
||||
has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
upsert_env_var() {
|
||||
local file="$1" key="$2" val="$3"
|
||||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||||
else
|
||||
echo "${key}=${val}" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
write_1panel_hosts_volume() {
|
||||
local mysql_host="${1:-}" redis_host="${2:-}"
|
||||
[[ -n "$mysql_host" || -n "$redis_host" ]] || return 0
|
||||
local vol json
|
||||
vol="$(docker volume ls -q --filter name=nexus-web-data 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "$vol" ]]; then
|
||||
warn "未找到 nexus-web-data 卷,跳过写入 1panel-hosts.json"
|
||||
return 0
|
||||
fi
|
||||
json="{"
|
||||
if [[ -n "$mysql_host" ]]; then
|
||||
json+="\"db_host\":\"${mysql_host}\""
|
||||
[[ -n "$redis_host" ]] && json+=","
|
||||
fi
|
||||
if [[ -n "$redis_host" ]]; then
|
||||
json+="\"redis_host\":\"${redis_host}\""
|
||||
fi
|
||||
json+="}"
|
||||
if printf '%s' "$json" | docker run --rm -i -v "${vol}:/data" alpine:3.19 \
|
||||
sh -c 'cat > /data/1panel-hosts.json && chmod 644 /data/1panel-hosts.json'; then
|
||||
info "已写入卷 ${vol} → web/data/1panel-hosts.json"
|
||||
else
|
||||
warn "写入 1panel-hosts.json 失败(向导仍可读 .env.prod 环境变量)"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_1panel_service_hosts() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local detect="$root/deploy/detect-1panel-services.sh"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
if ! has_1panel_network; then
|
||||
warn "未找到 1panel-network — 请先在 1Panel 应用商店安装 MySQL/Redis"
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
chmod +x "$detect" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
warn "未找到 detect-1panel-services.sh,跳过 1Panel 容器名探测"
|
||||
return 0
|
||||
fi
|
||||
local line mysql_host="" redis_host="" detect_out
|
||||
if ! detect_out="$("$detect" 2>&1)"; then
|
||||
warn "1Panel 容器探测: ${detect_out:-无 MySQL/Redis 容器运行}"
|
||||
return 0
|
||||
fi
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_1PANEL_DB_HOST=*)
|
||||
mysql_host="${line#NEXUS_1PANEL_DB_HOST=}"
|
||||
upsert_env_var "$env_file" NEXUS_1PANEL_DB_HOST "$mysql_host"
|
||||
info "1Panel MySQL 容器: $mysql_host(安装向导将预填为数据库主机)"
|
||||
;;
|
||||
NEXUS_1PANEL_REDIS_HOST=*)
|
||||
redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}"
|
||||
upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host"
|
||||
upsert_env_var "$env_file" "NEXUS_REDIS_URL" "redis://${redis_host}:6379/0"
|
||||
info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 已同步)"
|
||||
;;
|
||||
esac
|
||||
done <<<"$detect_out"
|
||||
write_1panel_hosts_volume "$mysql_host" "$redis_host"
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
local root="$1"
|
||||
shift
|
||||
@@ -195,7 +275,11 @@ compose_cmd() {
|
||||
exit 1
|
||||
fi
|
||||
cd "$root"
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
local -a compose_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$COMPOSE_1PANEL" ]]; then
|
||||
compose_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
docker compose "${compose_args[@]}" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
}
|
||||
|
||||
rand_secret() {
|
||||
@@ -449,8 +533,6 @@ write_temp_secrets_env() {
|
||||
local f
|
||||
f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
|
||||
{
|
||||
printf 'MYSQL_ROOT_PASSWORD=%s\n' "${MYSQL_ROOT_PASSWORD:-$(rand_pass)}"
|
||||
printf 'MYSQL_PASSWORD=%s\n' "${MYSQL_PASSWORD:-$(rand_pass)}"
|
||||
printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
|
||||
printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
|
||||
printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
|
||||
@@ -497,7 +579,7 @@ write_env_prod() {
|
||||
--from-env "$FROM_ENV" \
|
||||
--backup "$backup"
|
||||
elif [[ "$FRESH_INSTALL" == true ]]; then
|
||||
info "全新安装:为本机生成唯一 MYSQL / NEXUS 密钥..."
|
||||
info "全新安装:为本机生成唯一 NEXUS 密钥(MySQL/Redis 请自行安装)..."
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
@@ -539,12 +621,30 @@ write_env_prod() {
|
||||
info "已写入 $env_file"
|
||||
}
|
||||
|
||||
purge_legacy_bundled_services() {
|
||||
local root="$1"
|
||||
local s
|
||||
for s in \
|
||||
"$root/deploy/uninstall-mysql-compose.sh" \
|
||||
"$root/deploy/uninstall-redis-compose.sh"; do
|
||||
if [[ -x "$s" ]]; then
|
||||
step "清理遗留 Compose 内置服务: $(basename "$s")"
|
||||
NEXUS_ROOT="$root" bash "$s" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
compose_up() {
|
||||
local root="$1"
|
||||
save_host_profile "$root"
|
||||
info "档位: ${NEXUS_PROFILE}(MySQL/Nexus 内存与连接池已套用;Redis 请自行安装)"
|
||||
purge_legacy_bundled_services "$root"
|
||||
sync_1panel_service_hosts "$root"
|
||||
if has_1panel_network; then
|
||||
info "已接入 1panel-network(MySQL/Redis 使用 1Panel 容器名互联)"
|
||||
fi
|
||||
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
|
||||
info "构建并启动(首次约 10~20 分钟)..."
|
||||
compose_cmd "$root" up -d --build
|
||||
compose_cmd "$root" up -d --build --remove-orphans
|
||||
}
|
||||
|
||||
verify_install_wizard() {
|
||||
@@ -595,8 +695,8 @@ verify_health() {
|
||||
info "浏览器: https://${NEXUS_DOMAIN}/app/"
|
||||
fi
|
||||
echo ""
|
||||
info "运维菜单: nx 或 bash $root/deploy/nx god"
|
||||
info " (安装 nx 命令: ln -sf $root/deploy/nx /usr/local/bin/nx)"
|
||||
NEXUS_ROOT="$root" bash "$root/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
info "运维菜单: nx 或 nexus-update"
|
||||
check_ports_post "$root"
|
||||
return 0
|
||||
fi
|
||||
@@ -645,23 +745,55 @@ show_pending() {
|
||||
}
|
||||
|
||||
backup_mysql() {
|
||||
local root="$1" ts file mysql_root
|
||||
local root="$1" ts file db_url cname
|
||||
if [[ "$NO_BACKUP" == true ]]; then
|
||||
warn "已跳过备份 (--no-backup)"
|
||||
return 0
|
||||
fi
|
||||
mysql_root="$(read_env_val "$root/$ENV_PROD" MYSQL_ROOT_PASSWORD)"
|
||||
if [[ -z "$mysql_root" ]]; then
|
||||
warn "无法读取 MYSQL_ROOT_PASSWORD,跳过备份"
|
||||
if ! command -v mysqldump >/dev/null 2>&1; then
|
||||
warn "未安装 mysqldump,跳过 MySQL 备份"
|
||||
return 0
|
||||
fi
|
||||
cname="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
db_url=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
db_url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
fi
|
||||
if [[ -z "$db_url" ]]; then
|
||||
warn "未找到 NEXUS_DATABASE_URL(外置 MySQL 需先完成安装向导),跳过备份"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$NEXUS_BACKUP_DIR"
|
||||
ts="$(date +%Y%m%d-%H%M%S)"
|
||||
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
|
||||
step "备份 MySQL → $file"
|
||||
if ! compose_cmd "$root" exec -T mysql \
|
||||
mysqldump -uroot -p"${mysql_root}" --single-transaction --routines --triggers nexus \
|
||||
>"$file" 2>/dev/null; then
|
||||
step "备份外置 MySQL → $file"
|
||||
if ! python3 - "$db_url" "$file" <<'PY'
|
||||
import sys
|
||||
import urllib.parse
|
||||
from subprocess import run
|
||||
|
||||
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
||||
out = sys.argv[2]
|
||||
p = urllib.parse.urlparse(url)
|
||||
user = urllib.parse.unquote(p.username or "")
|
||||
password = urllib.parse.unquote(p.password or "")
|
||||
host = p.hostname or "127.0.0.1"
|
||||
if host == "host.docker.internal":
|
||||
host = "127.0.0.1"
|
||||
port = str(p.port or 3306)
|
||||
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
||||
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
||||
cmd = [
|
||||
"mysqldump", "-h", host, "-P", port, "-u", user,
|
||||
"--single-transaction", "--routines", "--triggers", "--set-gtid-purged=OFF", db,
|
||||
]
|
||||
r = run(cmd, env=env, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
||||
sys.exit(r.returncode)
|
||||
open(out, "w", encoding="utf-8").write(r.stdout)
|
||||
PY
|
||||
then
|
||||
error "mysqldump 失败,已中止"
|
||||
rm -f "$file"
|
||||
exit 1
|
||||
@@ -706,6 +838,7 @@ cmd_install() {
|
||||
ensure_docker
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
clone_or_pull
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
write_env_prod "$NEXUS_ROOT"
|
||||
compose_up "$NEXUS_ROOT"
|
||||
verify_health "$NEXUS_ROOT" "安装"
|
||||
@@ -726,10 +859,8 @@ cmd_upgrade() {
|
||||
resolve_profile
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
git_fetch
|
||||
show_pending || true
|
||||
|
||||
if ! show_pending; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$CHECK_ONLY" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -738,15 +869,35 @@ cmd_upgrade() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local behind
|
||||
behind="$(commits_behind)"
|
||||
if [[ "$behind" != "0" ]]; then
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
git_upgrade
|
||||
else
|
||||
info "Git 已最新,仍重建 Docker 镜像与容器(避免旧 entrypoint 缓存)"
|
||||
if [[ "$NO_BACKUP" != true ]]; then
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
fi
|
||||
fi
|
||||
|
||||
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
|
||||
save_host_profile "$NEXUS_ROOT"
|
||||
step "重建容器..."
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build
|
||||
purge_legacy_bundled_services "$NEXUS_ROOT"
|
||||
sync_1panel_service_hosts "$NEXUS_ROOT"
|
||||
if [[ "$NO_CACHE" == true ]]; then
|
||||
step "无缓存重建 Nexus 镜像..."
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
fi
|
||||
step "重建容器(--remove-orphans 清理旧 mysql/redis)..."
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans
|
||||
if [[ -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" || true
|
||||
fi
|
||||
if [[ "$PRUNE_IMAGES" == true ]]; then
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
fi
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
verify_health "$NEXUS_ROOT" "升级"
|
||||
}
|
||||
|
||||
@@ -793,6 +944,7 @@ parse_common_upgrade() {
|
||||
--branch) GIT_BRANCH="$2"; shift 2 ;;
|
||||
--no-backup) NO_BACKUP=true; shift ;;
|
||||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||||
--no-cache|--rebuild) NO_CACHE=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--check) CHECK_ONLY=true; shift ;;
|
||||
*)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus NX — 一键安装 + 上帝菜单(1Panel Docker 生产栈)
|
||||
# Nexus NX — 统一运维菜单(安装 + 升级 + 日常运维)
|
||||
#
|
||||
# bash deploy/nx 交互菜单
|
||||
# bash deploy/nx install 同菜单「安装」
|
||||
# bash deploy/nx god 上帝菜单(运维)
|
||||
# nx 安装后链到 /usr/local/bin/nx
|
||||
# nx 交互菜单
|
||||
# nx update 一键更新(等同 deploy/update.sh)
|
||||
# nx update --no-cache 无缓存重建镜像
|
||||
# nx install-fresh 全新安装
|
||||
# nx health 健康检查
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# 经 /usr/local/bin/nx 符号链接调用时,须 readlink 才能找到 deploy/ 目录
|
||||
NX_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
NX_DIR="$(cd "$(dirname "$NX_SELF")" && pwd)"
|
||||
# shellcheck source=./nexus-1panel.sh
|
||||
@@ -50,11 +50,8 @@ detect_runtime() {
|
||||
}
|
||||
|
||||
install_nx_cli() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
ln -sf "${NX_DIR}/nx" /usr/local/bin/nx 2>/dev/null || true
|
||||
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
|
||||
NEXUS_ROOT="$(cd "${NX_DIR}/.." && pwd)" \
|
||||
bash "${NX_DIR}/install-nx-cli.sh" 2>/dev/null || true
|
||||
}
|
||||
|
||||
menu_header() {
|
||||
@@ -62,7 +59,7 @@ menu_header() {
|
||||
clear_screen
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Nexus NX — ${title}"
|
||||
printf "║ Nexus NX — %-44s║\n" "$title"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
|
||||
if is_stack_installed; then
|
||||
@@ -99,7 +96,7 @@ run_install() {
|
||||
|
||||
menu_install() {
|
||||
while true; do
|
||||
menu_header "安装向导"
|
||||
menu_header "安装"
|
||||
cat <<'EOF'
|
||||
[1] 一键安装(默认 2核8G / 2c8g)
|
||||
[2] 安装 — 4核16G(4c16g)
|
||||
@@ -108,9 +105,7 @@ menu_install() {
|
||||
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone)
|
||||
[6] 全新安装(install-nexus-fresh.sh → /app/install.html)
|
||||
[7] 检查端口 / 防火墙(不安装)
|
||||
[8] 写入 Gitea 令牌(init-token)
|
||||
[9] 返回主菜单
|
||||
[0] 退出
|
||||
[0] 返回
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
@@ -142,19 +137,13 @@ EOF
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
read -r -p "Gitea Access Token: " tok
|
||||
cmd_init_token "$tok"
|
||||
pause_enter
|
||||
;;
|
||||
9) return 0 ;;
|
||||
0) exit 0 ;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项" ; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
god_restart_nexus() {
|
||||
ops_restart_nexus() {
|
||||
step "重启 Nexus(Python)..."
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" restart nexus
|
||||
@@ -164,32 +153,31 @@ god_restart_nexus() {
|
||||
error "未检测到运行中的 Nexus"
|
||||
return 1
|
||||
fi
|
||||
info "已发送重启"
|
||||
sleep 3
|
||||
curl -sf "http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")/health" && info "/health → ok" || warn "健康检查未通过,请查看日志"
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
god_restart_stack() {
|
||||
step "重启全部 Compose 服务..."
|
||||
ops_restart_stack() {
|
||||
step "重启 Compose 栈..."
|
||||
compose_cmd "$NEXUS_ROOT" restart
|
||||
sleep 5
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
god_stop_stack() {
|
||||
warn "将停止 MySQL / Redis / Nexus 全部容器"
|
||||
ops_stop_stack() {
|
||||
warn "将停止 Nexus 容器(MySQL/Redis 为外置服务,不受影响)"
|
||||
read -r -p "确认? [y/N]: " c
|
||||
[[ "$c" == "y" || "$c" == "Y" ]] || return 0
|
||||
compose_cmd "$NEXUS_ROOT" stop
|
||||
info "已停止"
|
||||
}
|
||||
|
||||
god_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d
|
||||
ops_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d --remove-orphans
|
||||
verify_health "$NEXUS_ROOT" "启动" || true
|
||||
}
|
||||
|
||||
god_logs() {
|
||||
ops_logs() {
|
||||
local svc="${1:-nexus}"
|
||||
echo ""
|
||||
read -r -p "跟踪日志行数 [80]: " n
|
||||
@@ -197,146 +185,159 @@ god_logs() {
|
||||
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
|
||||
}
|
||||
|
||||
god_status() {
|
||||
ops_status() {
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" ps
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl status nexus
|
||||
fi
|
||||
echo ""
|
||||
local port h
|
||||
local port h code
|
||||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
h=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "fail")
|
||||
info "/health → ${h}"
|
||||
curl -s -o /dev/null -w " /app/ HTTP %{http_code}\n" "http://127.0.0.1:${port}/app/" 2>/dev/null || true
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
info "/app/install.html → HTTP ${code}"
|
||||
}
|
||||
|
||||
god_backup_now() {
|
||||
ops_backup_now() {
|
||||
NO_BACKUP=false
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
}
|
||||
|
||||
god_upgrade_interactive() {
|
||||
ops_update() {
|
||||
local args=()
|
||||
PRUNE_IMAGES=false
|
||||
NO_BACKUP=false
|
||||
NO_CACHE=false
|
||||
read -r -p "跳过 MySQL 备份? [y/N]: " nb
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && NO_BACKUP=true
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && args+=(--no-backup)
|
||||
read -r -p "无缓存重建镜像? [y/N]: " nc
|
||||
[[ "$nc" == "y" || "$nc" == "Y" ]] && args+=(--no-cache)
|
||||
read -r -p "升级后清理悬空镜像? [y/N]: " pr
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && PRUNE_IMAGES=true
|
||||
cmd_upgrade
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && args+=(--prune)
|
||||
bash "${NX_DIR}/update.sh" "${args[@]}"
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_god() {
|
||||
require_installed() {
|
||||
if ! is_stack_installed; then
|
||||
warn "尚未安装,请先使用安装向导"
|
||||
warn "尚未安装,请先使用菜单 [1] 安装"
|
||||
pause_enter
|
||||
return 0
|
||||
return 1
|
||||
fi
|
||||
require_root
|
||||
load_secrets
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
|
||||
while true; do
|
||||
menu_header "上帝菜单 · 运维"
|
||||
cat <<EOF
|
||||
── 服务 ──
|
||||
[1] 重启 Nexus(Python 应用)
|
||||
[2] 重启全部容器(mysql + redis + nexus)
|
||||
[3] 启动 Compose 栈
|
||||
[4] 停止 Compose 栈
|
||||
[5] 重建 Nexus 镜像并启动(up -d --build nexus)
|
||||
|
||||
── 观测 ──
|
||||
[6] 状态 + 健康检查
|
||||
[7] 查看 Nexus 日志(跟踪)
|
||||
[8] 查看 MySQL / Redis 日志
|
||||
[9] 端口 / 防火墙检查
|
||||
|
||||
── 发布 ──
|
||||
[a] 检查 Git 更新
|
||||
[b] 一键升级(备份 + pull + 重建)
|
||||
[c] 立即备份 MySQL
|
||||
|
||||
── 其他 ──
|
||||
[d] 进入 Nexus 容器 shell
|
||||
[e] 打开 1Panel 反代说明
|
||||
[0] 返回主菜单
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) god_restart_nexus; pause_enter ;;
|
||||
2) god_restart_stack; pause_enter ;;
|
||||
3) god_start_stack; pause_enter ;;
|
||||
4) god_stop_stack; pause_enter ;;
|
||||
5)
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build nexus
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
6) god_status; pause_enter ;;
|
||||
7) god_logs nexus ;;
|
||||
8)
|
||||
echo "1) mysql 2) redis"
|
||||
read -r -p "选: " s
|
||||
[[ "$s" == "1" ]] && god_logs mysql || god_logs redis
|
||||
;;
|
||||
9)
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B) god_upgrade_interactive ;;
|
||||
c|C) god_backup_now; pause_enter ;;
|
||||
d)
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
e)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例配置: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_main() {
|
||||
load_secrets
|
||||
while true; do
|
||||
menu_header "主菜单"
|
||||
if is_stack_installed; then
|
||||
echo " 提示: 安装完成后请用 [2] 上帝菜单 重启 Python / 升级 / 看日志"
|
||||
echo ""
|
||||
fi
|
||||
menu_header "运维菜单"
|
||||
cat <<'EOF'
|
||||
[1] 安装向导(新机 / 迁机)
|
||||
[2] 上帝菜单(重启 Python · 升级 · 日志 · 备份)
|
||||
[3] 快速健康检查
|
||||
── 安装 ──
|
||||
[1] 安装向导(新机 / 迁机 / 档位)
|
||||
[2] 全新安装 → /app/install.html
|
||||
|
||||
── 日常运维(需已安装)──
|
||||
[3] 状态 + 健康检查
|
||||
[4] 一键更新(pull + 重建镜像)★
|
||||
[5] 重启 Nexus(Python)
|
||||
[6] 查看 Nexus 日志
|
||||
[7] 启动 / 停止 Compose 栈
|
||||
[8] 重建镜像并启动(--build nexus)
|
||||
[9] 备份 MySQL
|
||||
|
||||
── 其它 ──
|
||||
[a] 检查 Git 是否有更新
|
||||
[b] 端口 / 防火墙检查
|
||||
[c] 进入 Nexus 容器 shell
|
||||
[d] 1Panel 反代说明
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) menu_install ;;
|
||||
2) menu_god ;;
|
||||
2)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
;;
|
||||
3)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
god_status
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
ops_status
|
||||
else
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
4)
|
||||
require_installed || continue
|
||||
ops_update
|
||||
;;
|
||||
5)
|
||||
require_installed || continue
|
||||
ops_restart_nexus
|
||||
pause_enter
|
||||
;;
|
||||
6)
|
||||
require_installed || continue
|
||||
ops_logs nexus
|
||||
;;
|
||||
7)
|
||||
require_installed || continue
|
||||
echo " [1] 启动 [2] 停止 [3] 重启栈"
|
||||
read -r -p "选: " s
|
||||
case "$s" in
|
||||
1) ops_start_stack ;;
|
||||
2) ops_stop_stack ;;
|
||||
3) ops_restart_stack ;;
|
||||
*) warn "无效" ;;
|
||||
esac
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
require_installed || continue
|
||||
read -r -p "无缓存重建? [y/N]: " nc
|
||||
if [[ "$nc" == "y" || "$nc" == "Y" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
fi
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans nexus
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
9)
|
||||
require_installed || continue
|
||||
ops_backup_now
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
require_root
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B)
|
||||
require_root
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && check_ports_post "$NEXUS_ROOT" || true
|
||||
pause_enter
|
||||
;;
|
||||
c|C)
|
||||
require_installed || continue
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
d|D)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
@@ -345,11 +346,21 @@ EOF
|
||||
|
||||
nx_main() {
|
||||
local sub="${1:-}"
|
||||
install_nx_cli
|
||||
load_secrets
|
||||
case "$sub" in
|
||||
""|menu) menu_main ;;
|
||||
""|menu|god|ops|admin)
|
||||
# god/ops 已合并进主菜单,保留别名兼容
|
||||
[[ "$sub" == "god" || "$sub" == "ops" || "$sub" == "admin" ]] && \
|
||||
warn "「上帝菜单」已合并进 nx 主菜单,直接选对应项即可"
|
||||
menu_main
|
||||
;;
|
||||
install) menu_install ;;
|
||||
god|ops|admin) menu_god ;;
|
||||
update|upgrade)
|
||||
require_root
|
||||
shift || true
|
||||
exec bash "${NX_DIR}/update.sh" "$@"
|
||||
;;
|
||||
install-fresh|fresh)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
|
||||
@@ -364,24 +375,26 @@ nx_main() {
|
||||
;;
|
||||
health)
|
||||
require_root
|
||||
is_stack_installed && god_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
用法: nx [子命令]
|
||||
|
||||
(无) 交互主菜单
|
||||
install 安装向导
|
||||
install-fresh 全新安装(1Panel 风格,/app/install.html)
|
||||
install-auto 非交互安装已有密钥(2c8g)
|
||||
god 上帝菜单(重启/升级/日志)
|
||||
(无) 统一运维菜单(安装 + 升级 + 重启 + 日志)
|
||||
update 一键更新(等同 bash deploy/update.sh)
|
||||
install-fresh 全新安装(/app/install.html)
|
||||
install-auto 非交互安装(默认 2c8g)
|
||||
health 健康检查
|
||||
|
||||
底层仍可使用: bash deploy/nexus-1panel.sh install|upgrade|check|ports
|
||||
god / ops 已合并进主菜单(兼容别名)
|
||||
|
||||
curl 入口:
|
||||
安装 curl -fsSL .../deploy/quick-install.sh | bash
|
||||
更新 curl -fsSL .../deploy/update.sh | bash
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
# 兼容 nexus-1panel.sh 子命令
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
Regular → Executable
Regular → Executable
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnose 1Panel Redis auth from Nexus container network perspective.
|
||||
# Usage:
|
||||
# REDIS_PASS='redis_WP3NyN' bash deploy/test-1panel-redis.sh
|
||||
# bash deploy/test-1panel-redis.sh # prompts for password
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
ok() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
fail() { echo -e "\033[0;31m[FAIL]\033[0m $*"; }
|
||||
|
||||
pick_redis_container() {
|
||||
if [[ -n "${REDIS_CONTAINER:-}" ]]; then
|
||||
echo "$REDIS_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
local h
|
||||
h="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$h" ]]; then
|
||||
echo "$h"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
h="$("$NEXUS_ROOT/deploy/detect-1panel-services.sh" 2>/dev/null | sed -n 's/^NEXUS_1PANEL_REDIS_HOST=//p' | head -1)"
|
||||
[[ -n "$h" ]] && echo "$h" && return 0
|
||||
fi
|
||||
docker ps --format '{{.Names}}' | grep -iE '^1Panel-redis-' | head -1 || true
|
||||
}
|
||||
|
||||
try_ping() {
|
||||
local label="$1" url="$2"
|
||||
local nexus
|
||||
nexus="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$nexus" ]]; then
|
||||
fail "未找到 Nexus 容器"
|
||||
return 1
|
||||
fi
|
||||
if docker exec "$nexus" python3 -c "
|
||||
import redis
|
||||
r = redis.Redis.from_url('${url}', socket_timeout=3)
|
||||
print(r.ping())
|
||||
" 2>/dev/null | grep -q True; then
|
||||
ok "$label → $url"
|
||||
return 0
|
||||
fi
|
||||
fail "$label"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local redis_host pass
|
||||
redis_host="$(pick_redis_container)"
|
||||
[[ -n "$redis_host" ]] || { fail "未找到 Redis 容器/主机名"; exit 1; }
|
||||
info "Redis 主机: $redis_host"
|
||||
|
||||
if [[ -z "${REDIS_PASS:-}" ]]; then
|
||||
read -r -s -p "1Panel Redis 密码(应用参数): " pass
|
||||
echo ""
|
||||
else
|
||||
pass="$REDIS_PASS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "从 Nexus 容器内尝试多种 URL 格式(807f4c2 修复后)..."
|
||||
local any=0
|
||||
try_ping "无认证" "redis://${redis_host}:6379/0" && any=1 || true
|
||||
if [[ -n "$pass" ]]; then
|
||||
try_ping "仅密码 (:pass@)" "redis://:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
try_ping "default 用户" "redis://default:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
info "跳过 root 用户探测 — 1Panel Redis 无 root 账号"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$any" -eq 1 ]]; then
|
||||
ok "至少一种格式可用"
|
||||
echo ""
|
||||
info "安装向导步骤 3:1Panel Redis 无账号,只填密码(后端自动用 :pass@ 或 default)"
|
||||
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
|
||||
info " redis://:${pass}@${redis_host}:6379/0"
|
||||
info " 或 redis://default:${pass}@${redis_host}:6379/0"
|
||||
exit 0
|
||||
fi
|
||||
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
|
||||
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 MySQL 容器
|
||||
#
|
||||
# bash deploy/uninstall-mysql-compose.sh
|
||||
# bash deploy/uninstall-mysql-compose.sh --purge-volume
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 MySQL 容器
|
||||
|
||||
bash deploy/uninstall-mysql-compose.sh [--purge-volume]
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机/1Panel 自建的 MySQL
|
||||
- 卸载后请自建库 nexus + 用户,安装向导步骤 3 填写连接信息
|
||||
EOF
|
||||
}
|
||||
|
||||
compose_env_args() {
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
local prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
local p
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$(id -u)" -eq 0 ]] || { error "请使用 root 执行"; exit 1; }
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
for pattern in nexus-prod-mysql-1 nexus-prod-mysql nexus-mysql-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
|
||||
compose_env_args
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]] && grep -q '^ mysql:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 mysql 服务,尝试 compose rm..."
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop mysql 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f mysql 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
elif [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" && -f "${NEXUS_ROOT}/docker/.env.prod" ]]; then
|
||||
info "Compose --remove-orphans(从 nexus-prod 项目移除 mysql 孤儿容器)..."
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" up -d --remove-orphans 2>/dev/null || true)
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
leftover="$(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)"
|
||||
if [[ -n "$leftover" ]]; then
|
||||
warn "仍有 MySQL 相关容器:"
|
||||
echo "$leftover" | sed 's/^/ /'
|
||||
warn "若确认为 Compose 遗留,可: docker rm -f <容器名>"
|
||||
elif [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose MySQL 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose MySQL 容器。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "当前 Nexus 栈:"
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i nexus || docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
echo ""
|
||||
info "下一步:自建 MySQL 数据库与用户,安装向导步骤 3 使用 host.docker.internal:3306"
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 Redis 容器
|
||||
#
|
||||
# 适用:已从 docker-compose.prod.yml 移除 redis 服务,改用宿主机/1Panel 自建 Redis。
|
||||
# 不会卸载 apt/1Panel 安装的 Redis,仅删除 Docker 容器与(可选)数据卷。
|
||||
#
|
||||
# bash deploy/uninstall-redis-compose.sh
|
||||
# bash deploy/uninstall-redis-compose.sh --purge-volume # 同时删 redis-data 卷
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 Redis 容器
|
||||
|
||||
bash deploy/uninstall-redis-compose.sh [--purge-volume]
|
||||
|
||||
选项:
|
||||
--purge-volume 删除 nexus-prod_redis-data 数据卷(不可恢复)
|
||||
-h, --help 显示帮助
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机 redis-server / 1Panel 应用商店 Redis
|
||||
- 卸载后请在宿主机安装 Redis,安装向导步骤 2 须能 ping 通
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 常见 Compose 命名
|
||||
for pattern in nexus-prod-redis-1 nexus-prod-redis nexus-redis-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
# 名称含 nexus 且含 redis 的容器
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
|
||||
# 若旧 compose 仍定义 redis 服务
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]]; then
|
||||
if grep -q '^ redis:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 redis 服务,尝试 compose rm..."
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop redis 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f redis 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol(可能被占用)"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose Redis 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose Redis 容器(可能已删除)。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "下一步:在宿主机或 1Panel 安装 Redis,确认监听 6379:"
|
||||
echo " apt install -y redis-server && systemctl enable --now redis-server"
|
||||
echo " redis-cli ping # 期望 PONG"
|
||||
echo ""
|
||||
info "Docker 内 Nexus 连接宿主机 Redis:安装向导 / .env 使用 host.docker.internal:6379"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — 一键更新(拉代码 + 重建镜像 + 重启)
|
||||
#
|
||||
# 已在 /opt/nexus:
|
||||
# bash deploy/update.sh
|
||||
# bash deploy/update.sh --no-cache # 强制无缓存重建(entrypoint/Dockerfile 变更后)
|
||||
#
|
||||
# 远程一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||
|
||||
NEXUS_ROOT="$ROOT" bash "$ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
exec bash "$ROOT/deploy/nexus-1panel.sh" upgrade "$@"
|
||||
Regular → Executable
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify 1Panel + Docker install wizard readiness (run on the VPS as root).
|
||||
# See: deploy/README-1panel.md
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
COMPOSE_FILE="${NEXUS_ROOT}/docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="${NEXUS_ROOT}/docker/docker-compose.1panel.yml"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
info() { echo -e " $*"; }
|
||||
|
||||
FAILURES=0
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
fail "请用 root 运行(1Panel 终端默认 root)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_nexus_container() {
|
||||
NEXUS_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$NEXUS_CONTAINER" ]]; then
|
||||
fail "未找到运行中的 Nexus 容器"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
return 1
|
||||
fi
|
||||
pass "Nexus 容器: $NEXUS_CONTAINER"
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
fail "仓库不存在: $NEXUS_ROOT"
|
||||
return 1
|
||||
fi
|
||||
local sha msg
|
||||
sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
msg="$(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || true)"
|
||||
pass "Git: $msg"
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor b25d079 HEAD 2>/dev/null; then
|
||||
pass "已包含 b25d079(1Panel 容器名 sync 修复)"
|
||||
else
|
||||
warn "早于 b25d079 — 步骤 3 可能仍预填 host.docker.internal"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor 3b2856f HEAD 2>/dev/null; then
|
||||
pass "已包含 3b2856f+(Redis :pass@ 自动解析,无 root)"
|
||||
else
|
||||
warn "早于 3b2856f — Redis 认证可能仍误用 root"
|
||||
info "修复: cd $NEXUS_ROOT && git pull && bash deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_env_prod() {
|
||||
if [[ ! -f "$ENV_PROD" ]]; then
|
||||
fail "缺少 $ENV_PROD"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/install-nexus-fresh.sh --skip-clone"
|
||||
return 1
|
||||
fi
|
||||
pass "docker/.env.prod 存在"
|
||||
if grep -qE '^NEXUS_INSTALL_WIZARD_PENDING=1' "$ENV_PROD"; then
|
||||
pass "NEXUS_INSTALL_WIZARD_PENDING=1(安装模式)"
|
||||
else
|
||||
warn "NEXUS_INSTALL_WIZARD_PENDING≠1 — entrypoint 可能已写 /app/.env"
|
||||
fi
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
pass "NEXUS_1PANEL_DB_HOST=$db"
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_DB_HOST"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/detect-1panel-services.sh && nx update"
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
pass "NEXUS_1PANEL_REDIS_HOST=$redis"
|
||||
local rurl
|
||||
rurl="$(grep -E '^NEXUS_REDIS_URL=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ "$rurl" == *"$redis"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 已指向 Redis 容器"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 未含 $redis — nx update 会同步"
|
||||
fi
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_REDIS_HOST"
|
||||
info "修复: 在 1Panel 应用商店安装 Redis 后执行 nx update"
|
||||
fi
|
||||
}
|
||||
|
||||
check_1panel_network() {
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
fail "1panel-network 不存在"
|
||||
info "修复: 在 1Panel 应用商店安装 MySQL + Redis(会自动创建网络)"
|
||||
return 1
|
||||
fi
|
||||
pass "1panel-network 存在"
|
||||
if [[ -n "${NEXUS_CONTAINER:-}" ]]; then
|
||||
if docker inspect "$NEXUS_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' \
|
||||
| grep -q '1panel-network'; then
|
||||
pass "Nexus 已接入 1panel-network"
|
||||
else
|
||||
fail "Nexus 未接入 1panel-network"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
info "探测结果:"
|
||||
"$NEXUS_ROOT/deploy/detect-1panel-services.sh" | sed 's/^/ /' || true
|
||||
fi
|
||||
}
|
||||
|
||||
probe_from_container() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$db', 3306)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → MySQL $db:3306"
|
||||
else
|
||||
fail "容器内无法连接 MySQL $db:3306"
|
||||
info "检查 MySQL 容器是否运行、Nexus 是否在 1panel-network"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$redis', 6379)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → Redis $redis:6379"
|
||||
else
|
||||
fail "容器内无法连接 Redis $redis:6379"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_app_env_redis_url() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if ! docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
local rurl
|
||||
rurl="$(docker exec "$NEXUS_CONTAINER" grep -E '^NEXUS_REDIS_URL=' /app/.env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
if [[ -z "$rurl" ]]; then
|
||||
warn "/app/.env 无 NEXUS_REDIS_URL"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$rurl" == *"://root:"* ]]; then
|
||||
fail "NEXUS_REDIS_URL 含 root 用户(1Panel Redis 无 root 账号)"
|
||||
info "修复: redis://:密码@1Panel-redis-xxxx:6379/0 — 见 deploy/test-1panel-redis.sh"
|
||||
elif [[ "$rurl" == *"1Panel-redis"* ]] || [[ "$rurl" == *"://:"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 格式合理(容器名或 :pass@)"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 可能未指向 1Panel Redis: $rurl"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_mode() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
warn "/app/.env 已存在 — 安装向导可能已进入步骤 3+ 或误写 env"
|
||||
check_app_env_redis_url
|
||||
info "若需重来: 删卷 nexus-state 中 .env 并设 NEXUS_INSTALL_WIZARD_PENDING=1 后 nx update"
|
||||
else
|
||||
pass "/app/.env 不存在(安装模式 OK)"
|
||||
fi
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.install_locked 2>/dev/null; then
|
||||
fail "已锁定安装向导 — 应访问 /app/ 登录"
|
||||
fi
|
||||
local hosts_json
|
||||
hosts_json="$(docker exec "$NEXUS_CONTAINER" cat /app/web/data/1panel-hosts.json 2>/dev/null || true)"
|
||||
if [[ -n "$hosts_json" ]]; then
|
||||
pass "1panel-hosts.json: $hosts_json"
|
||||
else
|
||||
warn "无 1panel-hosts.json(依赖容器环境变量,nx update 会写入)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http_local() {
|
||||
local health code
|
||||
health="$(curl -sf http://127.0.0.1:8600/health 2>/dev/null || true)"
|
||||
if [[ "$health" == "ok" ]]; then
|
||||
pass "/health → ok"
|
||||
else
|
||||
fail "/health 非 ok(当前: ${health:-无响应})"
|
||||
info "修复: docker logs --tail=80 $NEXUS_CONTAINER"
|
||||
return 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/install.html 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]]; then
|
||||
pass "/app/install.html → HTTP 200"
|
||||
else
|
||||
fail "/app/install.html → HTTP $code"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_api() {
|
||||
local status env_json
|
||||
status="$(curl -sf http://127.0.0.1:8600/api/install/status 2>/dev/null || true)"
|
||||
if [[ -n "$status" ]]; then
|
||||
pass "/api/install/status → $status"
|
||||
if echo "$status" | grep -q '"installed":true'; then
|
||||
warn "installed=true — 向导已完成,请访问 /app/ 登录"
|
||||
fi
|
||||
else
|
||||
fail "/api/install/status 无响应"
|
||||
fi
|
||||
env_json="$(curl -sf http://127.0.0.1:8600/api/install/env-check 2>/dev/null || true)"
|
||||
if [[ -z "$env_json" ]]; then
|
||||
fail "/api/install/env-check 无响应"
|
||||
return 1
|
||||
fi
|
||||
if echo "$env_json" | grep -q 'docker_defaults'; then
|
||||
local db_host
|
||||
db_host="$(echo "$env_json" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print((d.get('docker_defaults') or {}).get('db_host',''))
|
||||
" 2>/dev/null || true)"
|
||||
if [[ -n "$db_host" && "$db_host" != host.docker.internal ]]; then
|
||||
pass "步骤 3 预填 db_host=$db_host"
|
||||
elif [[ "$db_host" == host.docker.internal ]]; then
|
||||
fail "步骤 3 仍预填 host.docker.internal"
|
||||
info "修复: nx update --no-cache(需 b25d079+)"
|
||||
fi
|
||||
else
|
||||
warn "env-check 无 docker_defaults(非 Docker 模式?)"
|
||||
fi
|
||||
if echo "$env_json" | grep -q '1Panel-mysql'; then
|
||||
pass "env-check MySQL 探测含 1Panel 容器名"
|
||||
elif echo "$env_json" | grep -q 'host.docker.internal'; then
|
||||
warn "env-check MySQL 仍指向 host.docker.internal"
|
||||
fi
|
||||
}
|
||||
|
||||
check_openresty_hint() {
|
||||
local domain code
|
||||
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo api.synaglobal.vip)"
|
||||
code="$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "https://${domain}/health" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]] && curl -sk "https://${domain}/health" 2>/dev/null | grep -q '^ok$'; then
|
||||
pass "HTTPS https://${domain}/health → ok"
|
||||
else
|
||||
warn "HTTPS https://${domain}/health → HTTP $code(外网可能未配 OpenResty 反代)"
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:8600"
|
||||
info "参考: $NEXUS_ROOT/deploy/1panel/openresty-nexus.conf.example"
|
||||
fi
|
||||
}
|
||||
|
||||
print_next_steps() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 安装向导后续步骤"
|
||||
echo "=========================================="
|
||||
echo "1. 1Panel 数据库 → 建库 nexus、用户 nexus(仅授权 nexus 库)"
|
||||
echo "2. 浏览器打开: https://$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo '你的域名')/app/install.html"
|
||||
echo "3. 步骤 2:MySQL/Redis TCP 应显示 ✓(容器名:端口)"
|
||||
echo "4. 步骤 3:确认 db_host/redis_host 为 1Panel-mysql/redis-xxxx,填 MySQL 密码"
|
||||
echo "5. 步骤 4:连接检测全绿 → 创建 admin"
|
||||
echo "6. 步骤 5:锁定安装"
|
||||
echo ""
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "先修复上方 [FAIL],再执行:"
|
||||
echo " cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
echo "Nexus 1Panel 安装向导验收 — $(date -Iseconds)"
|
||||
echo "NEXUS_ROOT=$NEXUS_ROOT"
|
||||
echo ""
|
||||
|
||||
check_repo || true
|
||||
check_env_prod || true
|
||||
resolve_nexus_container || true
|
||||
check_1panel_network || true
|
||||
probe_from_container || true
|
||||
check_install_mode || true
|
||||
check_http_local || true
|
||||
check_install_api || true
|
||||
check_openresty_hint || true
|
||||
print_next_steps
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo ""
|
||||
fail "合计 $FAILURES 项未通过"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
pass "验收通过 — 可打开安装向导完成步骤 1–5"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,9 +1,6 @@
|
||||
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates unique secrets per host)
|
||||
# Migration: bash deploy/nexus-1panel.sh install --from-env /path/to/old/.env
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change_me_root_strong
|
||||
MYSQL_PASSWORD=change_me_nexus_strong
|
||||
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
|
||||
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
|
||||
|
||||
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
|
||||
NEXUS_SECRET_KEY=
|
||||
@@ -17,12 +14,15 @@ NEXUS_INSTALL_WIZARD_PENDING=0
|
||||
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
|
||||
# 2 vCPU / 8 GiB host (matches docker/mysql-prod-2c8g.cnf + compose limits)
|
||||
# Nexus container resource profile (docker/profiles/*.env)
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# Redis 不在 Docker 栈内 — 请在宿主机或 1Panel 应用商店自行安装
|
||||
# Nexus 容器通过 host.docker.internal 访问宿主机 Redis(见 docker-compose.prod.yml)
|
||||
# 1Panel:nx update 自动探测并写入(安装向导步骤 3 预填)
|
||||
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
|
||||
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
|
||||
|
||||
# 外置 Redis(非 1Panel 时用 host.docker.internal)
|
||||
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||
|
||||
# Optional
|
||||
|
||||
+8
-7
@@ -41,15 +41,16 @@ docker compose build --no-cache nexus
|
||||
|
||||
## 服务
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| nexus | 8600 | FastAPI + 静态 `/app/` |
|
||||
| mysql | 3306 | 数据库 `nexus`(Compose 内) |
|
||||
| redis | — | **不在 Compose 内**;宿主机/1Panel 自建,默认 `host.docker.internal:6379` |
|
||||
| 环境 | Compose 服务 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 本地开发 `docker-compose.yml` | nexus + mysql + redis | 一键本地联调 |
|
||||
| 生产 `docker-compose.prod.yml` | **仅 nexus** | MySQL/Redis 宿主机或 1Panel 自建 |
|
||||
|
||||
生产 1Panel:`nx update` 自动写入 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`;MySQL 由安装向导写入 `NEXUS_DATABASE_URL`。
|
||||
|
||||
## 生产注意
|
||||
|
||||
- 修改 `MYSQL_*` 与 `NEXUS_*` 为强密码,勿使用 `generate_env.py` 默认值上生产。
|
||||
- 修改 `NEXUS_*` 为强密码;MySQL 使用自建库强密码。
|
||||
- 建议前置 Nginx/TLS,仅暴露 443。
|
||||
- 多实例部署需共享 MySQL、Redis;后台任务由 Redis primary lock 保证单实例执行。
|
||||
|
||||
@@ -62,7 +63,7 @@ docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up
|
||||
|
||||
- 镜像:`Dockerfile.prod`(含 `npm ci` + `vite build` + **`web/app/install.html` 安装向导**)
|
||||
- 仅绑定 `127.0.0.1:8600`;OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
|
||||
- **2C8G 调优**:`docker/mysql-prod-2c8g.cnf`(buffer pool 1G、`max_connections=120`)+ 容器 `mem_limit` + `NEXUS_DB_POOL_SIZE=30`
|
||||
- **2C8G 调优**:`docker/profiles/2c8g.env`(`NEXUS_MEM_LIMIT` + `NEXUS_DB_POOL_SIZE=30`);外置 MySQL 可参考 `docker/mysql-prod-2c8g.cnf` 手工调优
|
||||
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
|
||||
- 一键安装:`bash deploy/install-1panel-docker.sh`
|
||||
- 一键升级:`bash deploy/upgrade-1panel-docker.sh`(`--check` 仅查更新)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 1Panel overlay — join Nexus to 1panel-network (App Store MySQL/Redis use this network).
|
||||
# Applied automatically when `docker network inspect 1panel-network` succeeds (deploy/nexus-1panel.sh).
|
||||
|
||||
services:
|
||||
nexus:
|
||||
networks:
|
||||
- 1panel-network
|
||||
environment:
|
||||
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
|
||||
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
name: 1panel-network
|
||||
@@ -1,9 +1,8 @@
|
||||
# Nexus 6.0 — production Compose (1Panel / bare Docker host)
|
||||
# Resource profile: docker/profiles/{1c4g,2c8g,4c16g}.env — see deploy/nexus-1panel.sh
|
||||
# MySQL / Redis 不在 Compose 内 — 请在宿主机或 1Panel 自行安装
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/nexus-1panel.sh install --profile 2c8g
|
||||
# # or manually:
|
||||
# docker compose -f docker/docker-compose.prod.yml \
|
||||
# --env-file docker/.env.prod --env-file docker/profiles/2c8g.env up -d --build
|
||||
#
|
||||
@@ -12,51 +11,25 @@
|
||||
name: nexus-prod
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: nexus
|
||||
MYSQL_USER: nexus
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./${MYSQL_CNF_FILE:-mysql-prod-2c8g.cnf}:/etc/mysql/conf.d/99-nexus-prod.cnf:ro
|
||||
mem_limit: ${MYSQL_MEM_LIMIT:-2g}
|
||||
cpus: ${MYSQL_CPUS:-1.5}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 40s
|
||||
networks:
|
||||
- nexus-internal
|
||||
|
||||
nexus:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.prod
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: "3306"
|
||||
NEXUS_PERSIST_DIR: /var/lib/nexus
|
||||
NEXUS_DOCKER_WRITE_ENV: "1"
|
||||
NEXUS_HOST: "0.0.0.0"
|
||||
NEXUS_PORT: "8600"
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD}@mysql:3306/nexus
|
||||
# Redis 不在 Compose 内 — 请在宿主机/1Panel 自建,默认连宿主机 6379
|
||||
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://host.docker.internal:6379/0}
|
||||
# 1Panel: nx update 写入 redis://1Panel-redis-xxxx:6379/0;非 1Panel 回退 host.docker.internal
|
||||
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://${NEXUS_1PANEL_REDIS_HOST:-host.docker.internal}:6379/0}
|
||||
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
|
||||
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
|
||||
@@ -84,6 +57,5 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
|
||||
@@ -86,14 +86,15 @@ persist_env_on_exit() {
|
||||
|
||||
trap persist_env_on_exit EXIT INT TERM
|
||||
|
||||
MYSQL_HOST="${MYSQL_HOST:-mysql}"
|
||||
# MySQL / Redis 均由用户自行安装在宿主机;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_MYSQL_WAIT:-0}" != "1" ]; then
|
||||
MYSQL_HOST="${NEXUS_1PANEL_DB_HOST:-${MYSQL_HOST:-host.docker.internal}}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
wait_tcp_optional "${MYSQL_HOST}" "${MYSQL_PORT}" 30
|
||||
fi
|
||||
|
||||
wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}"
|
||||
|
||||
# Redis 由用户自行安装在宿主机/1Panel;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
|
||||
REDIS_HOST="${REDIS_HOST:-host.docker.internal}"
|
||||
REDIS_HOST="${NEXUS_1PANEL_REDIS_HOST:-${REDIS_HOST:-host.docker.internal}}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# Host profile: 1 vCPU / 4 GiB (test / small VPS)
|
||||
MYSQL_CNF_FILE=mysql-prod-1c4g.cnf
|
||||
MYSQL_MEM_LIMIT=1g
|
||||
MYSQL_CPUS=1
|
||||
# Host profile: 1 vCPU / 4 GiB
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# Host profile: 2 vCPU / 8 GiB — default production
|
||||
MYSQL_CNF_FILE=mysql-prod-2c8g.cnf
|
||||
MYSQL_MEM_LIMIT=2g
|
||||
MYSQL_CPUS=1.5
|
||||
NEXUS_MEM_LIMIT=2g
|
||||
NEXUS_CPUS=1.5
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Host profile: 4 vCPU / 16 GiB
|
||||
MYSQL_CNF_FILE=mysql-prod-4c16g.cnf
|
||||
MYSQL_MEM_LIMIT=6g
|
||||
MYSQL_CPUS=3
|
||||
NEXUS_MEM_LIMIT=4g
|
||||
NEXUS_CPUS=3
|
||||
NEXUS_DB_POOL_SIZE=60
|
||||
NEXUS_DB_MAX_OVERFLOW=40
|
||||
NEXUS_DB_POOL_SIZE=50
|
||||
NEXUS_DB_MAX_OVERFLOW=30
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
| [project/risk-acceptance-single-operator.md](project/risk-acceptance-single-operator.md) | 单人运维风险接受 |
|
||||
| [project/script-execution.md](project/script-execution.md) | 脚本执行 |
|
||||
| [project/alert-push-policy.md](project/alert-push-policy.md) | 告警策略 |
|
||||
| [project/deploy.md](project/deploy.md) | 部署补充 |
|
||||
| [project/deploy.md](project/deploy.md) | 部署补充(宝塔/裸机) |
|
||||
| [project/nexus-1panel-operations-knowledge.md](project/nexus-1panel-operations-knowledge.md) | **1Panel + Docker 运维知识 SSOT** |
|
||||
| [project/production-verification-checklist.md](project/production-verification-checklist.md) | L5 清单 |
|
||||
|
||||
根目录 [AGENTS.md](../AGENTS.md) · [CLAUDE.md](../CLAUDE.md) · [.cursorrules](../.cursorrules) 为薄入口。
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 2026-06-04 — 生产 Compose 移除外置 MySQL
|
||||
|
||||
## 摘要
|
||||
|
||||
生产 Docker 栈从「MySQL + Nexus」改为**仅 Nexus 容器**;MySQL 与 Redis 均由用户在宿主机或 1Panel 自行安装,安装向导步骤 3 填写连接信息。
|
||||
|
||||
## 动机
|
||||
|
||||
- 用户希望数据库自行创建与管理,不随安装脚本附带 MySQL 容器与自动密码。
|
||||
- 与已移除外置 Redis Compose 服务的设计一致,降低 bind mount / 卷冲突风险。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `docker/docker-compose.prod.yml` | 移除 `mysql` 服务与数据卷 |
|
||||
| `docker/.env.prod.example` | 移除 `MYSQL_*` |
|
||||
| `docker/profiles/*.env` | 移除 `MYSQL_CNF_FILE` 等 |
|
||||
| `docker/entrypoint.sh` | MySQL 等待改为 `host.docker.internal`(可选) |
|
||||
| `scripts/generate_nexus_secrets.py` | 新机仅生成 `NEXUS_*` |
|
||||
| `deploy/nexus-1panel.sh` | 备份改读外置 `NEXUS_DATABASE_URL` |
|
||||
| `deploy/uninstall-mysql-compose.sh` | 卸载遗留 Compose MySQL 容器 |
|
||||
| `deploy/README-1panel.md` / `docker/README.md` | 文档更新 |
|
||||
| `web/app/install.html` | MySQL 自建说明 |
|
||||
| `server/api/install.py` | Docker 默认 `db_host=host.docker.internal` |
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/uninstall-mysql-compose.sh # 可选:删旧 Compose MySQL
|
||||
bash deploy/nexus-1panel.sh upgrade
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
自建 MySQL 后完成 `/app/install.html` 步骤 3(主机 `host.docker.internal`)。
|
||||
|
||||
## 验证
|
||||
|
||||
- `docker compose -f docker/docker-compose.prod.yml config` 仅含 `nexus` 服务
|
||||
- `python3 scripts/generate_nexus_secrets.py write-prod --fresh` 输出无 `MYSQL_*`
|
||||
- 安装向导步骤 3 可连接外置 MySQL 并完成 `init-db`
|
||||
- `docker ps` 无 `nexus-prod-mysql-*`;`nexus-1panel.sh upgrade` 自动 `--remove-orphans`
|
||||
|
||||
## 2026-06-04 补充
|
||||
|
||||
- `install` / `upgrade` 前自动执行 `uninstall-mysql-compose.sh`
|
||||
- `compose up` 增加 `--remove-orphans`,从 `nexus-prod` 项目移除已删服务的孤儿容器
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-06-05 — 修复 nx update 未同步 1Panel 容器名
|
||||
|
||||
## 摘要
|
||||
|
||||
用户步骤 3 仍填 `host.docker.internal`:`cmd_upgrade` 未调用 `sync_1panel_service_hosts`,容器环境变量与向导预填未更新。升级路径补 sync;探测改读 `1panel-network`;写入 `web/data/1panel-hosts.json` 作后备;向导加载时覆盖陈旧主机名。
|
||||
|
||||
## 根因
|
||||
|
||||
- `compose_up`(安装)会 sync,`cmd_upgrade`(日常 `nx update`)之前不会
|
||||
- 安装状态/表单可能缓存 `host.docker.internal`,覆盖 `docker_defaults`
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nexus-1panel.sh` — upgrade 调 sync + 写卷
|
||||
- `deploy/detect-1panel-services.sh` — 从 1panel-network 枚举容器
|
||||
- `server/api/install.py` — 读 `1panel-hosts.json` 后备
|
||||
- `web/app/install.html` — `refreshDockerDefaults` 覆盖陈旧主机
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
nx update --no-cache
|
||||
grep 1PANEL docker/.env.prod
|
||||
docker exec nexus-prod-nexus-1 cat /app/web/data/1panel-hosts.json
|
||||
# 安装向导步骤 3 db_host 应为 1Panel-mysql-xxxx
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# 2026-06-05 — 1Panel 1panel-network + 容器名互联
|
||||
|
||||
## 摘要
|
||||
|
||||
依据 [1Panel #11676](https://github.com/1Panel-dev/1Panel/issues/11676) 与 [appstore mysql/redis](https://github.com/1Panel-dev/appstore):应用商店 MySQL/Redis 在 `1panel-network`,桥接容器应使用**容器名**而非 `host.docker.internal`。Nexus 自动接入该网络并探测 `1Panel-mysql-*` / `1Panel-redis-*` 预填安装向导。
|
||||
|
||||
## 动机
|
||||
|
||||
用户步骤 3 报 `(2003) Can't connect to MySQL server on 'host.docker.internal'`:1Panel MySQL 未映射到 docker 网桥可达地址,或仅监听容器网络。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `docker/docker-compose.1panel.yml` — 接入 `1panel-network`
|
||||
- `deploy/detect-1panel-services.sh` — 探测容器名
|
||||
- `deploy/nexus-1panel.sh` — compose 叠加与 env 写入
|
||||
- `server/api/install.py` — 探测顺序与向导预填
|
||||
- `deploy/README-1panel.md`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
# 安装向导步骤 3 db_host 应为 1Panel-mysql-xxxx
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# 2026-06-05 — 1Panel Redis 容器名与 NEXUS_REDIS_URL 同步
|
||||
|
||||
## 摘要
|
||||
|
||||
与 MySQL 对齐:探测到 `1Panel-redis-*` 时同步更新 `NEXUS_REDIS_URL`;安装向导步骤 3 增加 Redis TCP 预检与 1panel-network 错误文案;Compose 默认 Redis URL 优先使用 `NEXUS_1PANEL_REDIS_HOST`。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nexus-1panel.sh` — sync 写入 `NEXUS_REDIS_URL`
|
||||
- `server/api/install.py` — Redis 预检与 `_redis_connect_error_detail`
|
||||
- `docker/docker-compose.prod.yml` — Redis URL 默认表达式
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash deploy/detect-1panel-services.sh # 应输出 NEXUS_1PANEL_REDIS_HOST=...
|
||||
grep NEXUS_REDIS_URL docker/.env.prod # redis://1Panel-redis-xxxx:6379/0
|
||||
nx update --no-cache
|
||||
# 安装向导步骤 3 redis_host 预填容器名;步骤 4 Redis 连接通过
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
# 2026-06-05 — 安装向导 MySQL TCP 检测与 2003 错误说明
|
||||
|
||||
## 摘要
|
||||
|
||||
Docker 部署下步骤 3 报 `(2003) Can't connect to MySQL server on 'host.docker.internal'` 多为宿主机未安装/未启动 MySQL。增加步骤 2 MySQL TCP 探测、init-db 前置 TCP 检查与可读错误文案;README-1panel 补充 1Panel 建库步骤。
|
||||
|
||||
## 动机
|
||||
|
||||
外置 MySQL 需用户在 1Panel 应用商店自行安装;原向导步骤 2 不检测 MySQL,步骤 3 直接 pymysql 报错难以理解。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — `_probe_mysql_installed`、init-db TCP 预检
|
||||
- `deploy/README-1panel.md` — 1Panel MySQL 安装与验证命令
|
||||
|
||||
## 验证
|
||||
|
||||
- 未装 MySQL:步骤 2 显示「未检测到 MySQL」;步骤 3 返回中文说明而非裸 pymysql 栈
|
||||
- 已装 MySQL:步骤 2 显示可访问;步骤 3 可正常建表
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-06-05 — 安装向导 Redis/MySQL 检测分步
|
||||
|
||||
## 摘要
|
||||
|
||||
- **步骤 2**:Redis 仅 TCP 检测宿主机是否已安装(`host.docker.internal:6379`),不阻塞进入步骤 3
|
||||
- **步骤 4**:新增 `GET /api/install/connection-check`,验证 `.env` 中 MySQL + Redis 真实连接
|
||||
- 创建管理员前须通过步骤 4 连接检测
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py`
|
||||
- `web/app/install.html`
|
||||
|
||||
## 验证
|
||||
|
||||
- 步骤 2:Redis 未装仍可进步骤 3;已装显示绿色
|
||||
- 步骤 3 完成后步骤 4 自动检测 MySQL/Redis
|
||||
- 连接失败时「创建账号」按钮禁用
|
||||
@@ -0,0 +1,14 @@
|
||||
# 2026-06-05 — 安装向导精简 Redis/MySQL 提示
|
||||
|
||||
## 摘要
|
||||
|
||||
Docker 部署已通过 `docker_defaults` 预填 `host.docker.internal`,移除步骤 3 中重复的「请先在 1Panel 安装」与主机填写说明;步骤 4 连接失败文案同步简化。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `web/app/install.html`
|
||||
- `server/api/install.py`(步骤 2 Redis 未检测文案)
|
||||
|
||||
## 验证
|
||||
|
||||
- Docker 环境打开 `/app/install.html` 步骤 3,MySQL/Redis 区无冗余灰字提示,主机仍为预填值
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-06-05 — 修复 nx Permission denied
|
||||
|
||||
## 摘要
|
||||
|
||||
`deploy/nx` 在 Git 中为 `100644`,`git pull` 后 `/usr/local/bin/nx` 符号链接指向不可执行文件,运行报 `Permission denied`。
|
||||
|
||||
## 修复
|
||||
|
||||
- Git 为 `deploy/nx`、`deploy/update.sh` 等脚本设置 `+x`
|
||||
- `deploy/update.sh` 启动时自动 `chmod +x` 并刷新 `/usr/local/bin/nx` 符号链接
|
||||
|
||||
## 服务器立即修复
|
||||
|
||||
```bash
|
||||
chmod +x /opt/nexus/deploy/nx /opt/nexus/deploy/update.sh
|
||||
ln -sf /opt/nexus/deploy/nx /usr/local/bin/nx
|
||||
nx
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-06-05 — nx 全局 CLI 注册
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/install-nx-cli.sh`,统一 `chmod +x` 与 `/usr/local/bin` 符号链接;`install`、`update`、`upgrade`、首次 `nx` 均自动调用,确保 `nx` 全局可用。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/install-nx-cli.sh`(新)
|
||||
- `deploy/nx`、`deploy/update.sh`、`deploy/nexus-1panel.sh`、`deploy/install-nexus-fresh.sh`
|
||||
|
||||
## 服务器
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nx-cli.sh
|
||||
which nx && nx --help
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-06-05 — NX 菜单合并 + README 脚本索引
|
||||
|
||||
## 摘要
|
||||
|
||||
- `deploy/nx`:原「主菜单」与「上帝菜单」合并为统一 `nx` 运维菜单;`[4] 一键更新` 调用 `deploy/update.sh`
|
||||
- `nx god` / `nx ops` 保留为兼容别名,进入同一菜单
|
||||
- 新增子命令:`nx update`(等同 `update.sh`)
|
||||
- 根目录 `README.md`、`deploy/README-1panel.md` 补充一键安装 / 一键更新 curl 表与脚本索引
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nx`
|
||||
- `README.md`
|
||||
- `deploy/README-1panel.md`
|
||||
- `deploy/install-nexus-fresh.sh`
|
||||
- `deploy/nexus-1panel.sh`
|
||||
|
||||
## 验证
|
||||
|
||||
- `bash -n deploy/nx`
|
||||
- 服务器 `nx` → `[4]` 触发 `update.sh`
|
||||
- Gitea 项目页 README 展示完整脚本表
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-06-05 — Redis 强制连通 + Compose Redis 卸载脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
- 恢复安装向导步骤 2:**Redis 未连接则不可进入步骤 3**
|
||||
- 新增 `deploy/uninstall-redis-compose.sh` 卸载遗留 `nexus-prod-redis` 容器
|
||||
|
||||
## 动机
|
||||
|
||||
用户自行安装 Redis,但环境检测必须验证连通;旧 Compose Redis 容器需单独卸载。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — Redis `pass: redis_ok`
|
||||
- `web/app/install.html` — 步骤 2「下一步」在 `!envAllPass` 时禁用
|
||||
- `deploy/uninstall-redis-compose.sh` — 新脚本
|
||||
- `deploy/README-1panel.md` — 文档
|
||||
|
||||
## 服务器操作
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/uninstall-redis-compose.sh
|
||||
apt install -y redis-server && systemctl enable --now redis-server
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# 2026-06-06 — 1Panel 安装向导服务器验收脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/verify-1panel-install-wizard.sh`,在 VPS 上一键检查 1panel-network、容器名 sync、安装模式与 `/api/install/env-check` 预填,便于完成 `/app/install.html` 五步向导前的排查。
|
||||
|
||||
## 动机
|
||||
|
||||
`b25d079` 已修复 `nx update` 同步 `NEXUS_1PANEL_*_HOST`,但外网 HTTPS/OpenResty 与服务器内网状态需分步验证;Agent 无法直连用户 VPS 时,需提供可复制的服务器侧验收命令。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/verify-1panel-install-wizard.sh` — 新建
|
||||
- `deploy/README-1panel.md` — 增加「安装向导验收」节
|
||||
|
||||
## 验证
|
||||
|
||||
在 1Panel 终端:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/verify-1panel-install-wizard.sh
|
||||
```
|
||||
|
||||
期望:`[PASS]` 含 b25d079、1panel-network、步骤 3 `db_host=1Panel-mysql-*`;失败项按脚本提示执行 `nx update --no-cache`。
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无 DB 迁移。仅文档与脚本,不影响运行中容器。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-06-06 — 1Panel MySQL 1045 授权修复
|
||||
|
||||
## 摘要
|
||||
|
||||
安装向导步骤 3 报 `Access denied for user 'nexus'@'172.18.x.x'`:1Panel 常见仅 `nexus@localhost`,Docker 网段连接被拒。新增 `fix-1panel-mysql-grant.sh` 与安装 API 明确错误提示。
|
||||
|
||||
## 动机
|
||||
|
||||
验收脚本已通过但 init-db 1045;需在仓库内提供可重复修复路径,避免手工 SQL 易错。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/fix-1panel-mysql-grant.sh` — 创建/授权 `nexus@'%'`
|
||||
- `server/api/install.py` — 1045 友好错误与 connection-check
|
||||
- `deploy/README-1panel.md` — 1045 排障节
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
MYSQL_ROOT_PASSWORD='...' NEXUS_DB_PASSWORD='...' bash deploy/fix-1panel-mysql-grant.sh
|
||||
# 安装向导步骤 3 初始化成功
|
||||
bash deploy/sync-install-wizard-to-container.sh # 同步 install.py 到运行中容器
|
||||
```
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无 DB 迁移。`install.py` 变更需 `sync-install-wizard-to-container.sh` 或 `nx update --no-cache`。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-06-06 — 1Panel 运维知识文档与 Redis 诊断脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `docs/project/nexus-1panel-operations-knowledge.md`:整合仓库内 1Panel 部署、安装向导五步、MySQL/Redis 网络与 URL 格式、`.env` 生命周期、故障决策树与提交索引。新增 `deploy/test-1panel-redis.sh` 从 Nexus 容器尝试多种 Redis URL 认证格式。
|
||||
|
||||
## 动机
|
||||
|
||||
用户 Redis 认证反复失败,需单文档承载 1Panel 场景(root 用户、`redis://root:pass@`)与运维脚本索引,避免信息分散在 changelog/对话中。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `docs/project/nexus-1panel-operations-knowledge.md` — 新建 SSOT
|
||||
- `deploy/test-1panel-redis.sh` — Redis 诊断
|
||||
- `docs/README.md`、`deploy/README-1panel.md` — 索引链接
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash -n deploy/test-1panel-redis.sh
|
||||
# 服务器: REDIS_PASS='...' bash deploy/test-1panel-redis.sh
|
||||
```
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 2026-06-06 — 安装向导 Redis 自动探测 :pass@ / default
|
||||
|
||||
## 摘要
|
||||
|
||||
实测 1Panel Redis(`1Panel-redis-1g6K`)支持 `redis://:pass@` 与 `default:pass`,**不支持 root**。向导取消预填 root;`test-credentials` / `init-db` 自动依次尝试多种 URL 并写入可用项。
|
||||
|
||||
## 动机
|
||||
|
||||
用户 `test-1panel-redis.sh` 显示 root 失败、`:pass@` 与 default 成功;旧版预填 root 导致步骤 3 检测失败。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — `_redis_url_candidates`、`_resolve_redis_url`
|
||||
- `web/app/install.html` — 用户名留空提示、清除 stale root
|
||||
- `deploy/test-1panel-redis.sh`、`docs/project/nexus-1panel-operations-knowledge.md`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_security_unit.py -k "redis_url or resolve_redis" -q
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
步骤 3:Redis 用户名留空 + 密码 → 检测应显示「仅密码 (:pass@)」通过。
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
`sync-install-wizard-to-container.sh` 或 `nx update`。已有错误 `.env` 手动改为 `redis://:pass@1Panel-redis-xxx:6379/0`。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 2026-06-06 — 安装向导 Redis:无账号 + 步骤 4 自愈
|
||||
|
||||
## 摘要
|
||||
|
||||
确立 1Panel Redis **无账号**铁律;Docker 安装模式仅尝试 `redis://:pass@` 与 `default`;步骤 4 `connection-check` 可自动修正错误的 `NEXUS_REDIS_URL`;前端 1Panel 模式隐藏 Redis 用户名字段。
|
||||
|
||||
## 动机
|
||||
|
||||
- 1Panel Redis 只有 `requirepass` 口令,不是 MySQL 式 `root` 用户;历史误读导致 `redis://root:pass@` 认证失败。
|
||||
- 步骤 3 在旧版本写入错误 URL 后,步骤 4 仅读 `.env` 原文无法通过,需向导内自愈。
|
||||
- 暴露「Redis 用户名」输入框易误导运维填 `root`。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — `_sanitize_redis_request`、候选顺序、`_init_db_request_from_redis_url`、`_patch_env_redis_url`、`connection-check` 修复
|
||||
- `web/app/install.html` — 1Panel 隐藏 `redis_user`、展示 `resolved_redis_url` / `redis_repaired`
|
||||
- `deploy/test-1panel-redis.sh` — 移除 root 探测
|
||||
- `deploy/verify-1panel-install-wizard.sh` — 3b2856f+ 与 `.env` Redis URL 检查
|
||||
- `docs/project/nexus-1panel-operations-knowledge.md` — 全文去 Redis root 表述
|
||||
- `tests/test_security_unit.py` — Redis 解析与 connection-check 修复用例
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
- 已有错误 `.env`:步骤 4 点「重新检测连接」可自动修补;或手动改为 `redis://:密码@1Panel-redis-xxxx:6379/0`
|
||||
- 部署:`git pull` → `bash deploy/sync-install-wizard-to-container.sh`(或 `nx update`)
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_security_unit.py -k redis
|
||||
bash deploy/test-1panel-redis.sh # 服务器,需 REDIS_PASS
|
||||
bash deploy/verify-1panel-install-wizard.sh
|
||||
```
|
||||
|
||||
浏览器:步骤 3 仅填 Redis 密码 → 检测 → 初始化 → 步骤 4 连接全绿。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 2026-06-06 — 修复安装向导 Redis URL 与 1Panel root 用户
|
||||
|
||||
## 摘要
|
||||
|
||||
步骤 3 Redis 连接失败根因:`_build_redis_url` 生成 `redis://pass@host`(密码被当成用户名);1Panel Redis ACL 用户名为 `root`。
|
||||
|
||||
## 修复
|
||||
|
||||
- URL 密码格式改为 `redis://:pass@host` 或 `redis://root:pass@host`
|
||||
- 向导新增 Redis 用户名,Docker/1Panel 预填 `root`
|
||||
- 错误提示说明 1Panel 用法
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py`
|
||||
- `web/app/install.html`
|
||||
- `tests/test_security_unit.py`
|
||||
- `deploy/README-1panel.md`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_security_unit.py -k build_redis_url -q
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
步骤 3:Redis 用户 `root`、密码 `redis_WP3NyN`、主机 `1Panel-redis-xxxx` → 检测通过。
|
||||
|
||||
## 已有 .env 时手动修正
|
||||
|
||||
```bash
|
||||
NEXUS_REDIS_URL="redis://root:你的密码@1Panel-redis-xxxx:6379/0"
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# 2026-06-06 — 安装向导步骤 3 账号密码预检
|
||||
|
||||
## 摘要
|
||||
|
||||
步骤 3 新增「检测 MySQL / Redis 连接」按钮;`init-db` 在写 `.env` 前强制校验账号密码,避免 TCP 通但 Redis/MySQL 认证失败仍进入步骤 4。
|
||||
|
||||
## 动机
|
||||
|
||||
用户反馈步骤 3 仅 TCP 检测,Redis 密码错误仍初始化成功,步骤 4 才失败且无法重回步骤 3。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — `POST /api/install/test-credentials`、`_credential_checks`、`init-db` 前置校验
|
||||
- `web/app/install.html` — 步骤 3 检测 UI,未通过禁用「初始化数据库」
|
||||
- `tests/test_security_unit.py` — 新端点单测
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_security_unit.py -k install_test_credentials -q
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
向导步骤 3:错误 Redis 密码 → 检测失败;修正后检测通过 → 可初始化。
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无 DB 迁移。部署后 `sync-install-wizard-to-container.sh` 或 `nx update --no-cache`。
|
||||
@@ -0,0 +1,378 @@
|
||||
# Nexus 6.0 — 1Panel + Docker 运维知识文档
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 版本 | 2026-06-06 |
|
||||
| 读者 | 运维、接续开发、AI Agent |
|
||||
| 定位 | **1Panel 生产部署 SSOT**(安装向导、网络、MySQL/Redis、日常 nx update) |
|
||||
| 关联 | [deploy/README-1panel.md](../../deploy/README-1panel.md) · [nexus-functional-development-guide.md](nexus-functional-development-guide.md) · [2026-06-04-1panel-docker-production.md](../design/plans/2026-06-04-1panel-docker-production.md) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构总览
|
||||
|
||||
### 1.1 生产拓扑(1Panel)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Internet --> OpenResty["OpenResty (host)"]
|
||||
OpenResty -->|"127.0.0.1:8600"| Nexus["Nexus 容器"]
|
||||
Nexus -->|"1Panel-mysql-xxxx:3306"| MySQL["1Panel MySQL"]
|
||||
Nexus -->|"1Panel-redis-xxxx:6379"| Redis["1Panel Redis"]
|
||||
Nexus --- VolState["卷 nexus-state (.env)"]
|
||||
Nexus --- VolData["卷 nexus-web-data"]
|
||||
MySQL --- Net["1panel-network"]
|
||||
Redis --- Net
|
||||
Nexus --- Net
|
||||
```
|
||||
|
||||
**铁律**([1Panel 官方说明](https://github.com/1Panel-dev/1Panel/issues/11676)):
|
||||
|
||||
- 应用商店 MySQL/Redis 在 **`1panel-network`** 上,容器间用 **容器名** 通信。
|
||||
- **禁止**在 Nexus 容器内用 `127.0.0.1`、`localhost`、`host.docker.internal` 连 1Panel MySQL/Redis。
|
||||
- OpenResty 在宿主机网络,反代目标为 **`http://127.0.0.1:8600`**。
|
||||
|
||||
### 1.2 仓库关键目录
|
||||
|
||||
| 路径 | 职责 |
|
||||
|------|------|
|
||||
| `server/` | FastAPI 后端;`api/install.py` = 安装向导 API |
|
||||
| `frontend/` | Vue 3 SPA 源码(14 页) |
|
||||
| `web/app/` | 构建产物;**`install.html` 为独立静态页**(Alpine.js,不经 Vite 路由) |
|
||||
| `web/data/` | 运行时:`config.json`、`1panel-hosts.json` |
|
||||
| `deploy/` | 1Panel 安装/升级、`nx`、验收与修复脚本 |
|
||||
| `docker/` | `docker-compose.prod.yml`、`entrypoint.sh`、`.env.prod` |
|
||||
| `Dockerfile.prod` | 多阶段:npm build + Python 镜像 |
|
||||
|
||||
### 1.3 两套 Compose
|
||||
|
||||
| 文件 | 服务 |
|
||||
|------|------|
|
||||
| `docker-compose.yml` | 本地 dev:nexus + mysql + redis |
|
||||
| `docker-compose.prod.yml` | **生产:仅 nexus** |
|
||||
| `docker-compose.1panel.yml` | 叠加:nexus 加入 `1panel-network` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 启动模式与 `.env` 生命周期
|
||||
|
||||
### 2.1 安装模式 vs 正常模式
|
||||
|
||||
`server/main.py` 中 `is_install_mode()`:**运行时**检测 `/app/.env` 是否存在。
|
||||
|
||||
| 模式 | 条件 | 可用能力 |
|
||||
|------|------|----------|
|
||||
| 安装模式 | 无 `/app/.env` | `/api/install/*`、`/app/install.html`、静态、`/health` |
|
||||
| 正常模式 | 有完整 `.env` | 全 API、WebSocket、后台任务 |
|
||||
|
||||
### 2.2 配置存储四层
|
||||
|
||||
| 层级 | 位置 | 写入方 |
|
||||
|------|------|--------|
|
||||
| Compose 环境 | 宿主机 `docker/.env.prod` | `nexus-1panel.sh install` / `generate_nexus_secrets.py` |
|
||||
| 应用根 | 容器 `/app/.env` | 安装向导步骤 3 `init-db` |
|
||||
| 持久卷 | `nexus-state` → `/var/lib/nexus/.env` | `entrypoint.sh` 退出时同步 |
|
||||
| Web 数据 | `nexus-web-data` → `/app/web/data/` | `config.json`、`1panel-hosts.json` |
|
||||
|
||||
**不可从 DB settings 覆盖**:`SECRET_KEY`、`API_KEY`、`ENCRYPTION_KEY`、`DATABASE_URL`。
|
||||
|
||||
### 2.3 Docker 安装状态机
|
||||
|
||||
```
|
||||
install --fresh
|
||||
→ docker/.env.prod: NEXUS_INSTALL_WIZARD_PENDING=1 + 预生成三密钥
|
||||
→ entrypoint 不写 /app/.env,不阻塞等待 MySQL/Redis
|
||||
|
||||
浏览器步骤 3 init-db
|
||||
→ 写 /app/.env、config.json、settings、.install_state.json
|
||||
|
||||
步骤 4 create-admin
|
||||
→ 写 admins,自动 lock(.install_locked)
|
||||
|
||||
entrypoint EXIT trap
|
||||
→ /app/.env 复制到 nexus-state
|
||||
```
|
||||
|
||||
**重要**:仅删除 1Panel 里的 MySQL 库 **不会** 清除 `/app/.env`;向导会提示「系统已初始化,无法重复执行数据库安装」。重装需删卷内 `.env` 并设 `NEXUS_INSTALL_WIZARD_PENDING=1`(见 §7.6)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 安装向导(5 步)
|
||||
|
||||
### 3.1 流程与 API
|
||||
|
||||
| 步 | UI | API | 说明 |
|
||||
|----|-----|-----|------|
|
||||
| 1 | 欢迎 | — | 准备项 |
|
||||
| 2 | 环境检测 | `GET /api/install/env-check` | TCP 探测 MySQL/Redis;返回 `docker_defaults` |
|
||||
| 3 | 数据库配置 | `POST /api/install/test-credentials` | **必须先检测账号密码** |
|
||||
| 3 | 初始化 | `POST /api/install/init-db` | 建表、三写、发 `install_token`;**.env 已存在则 403** |
|
||||
| 4 | 管理员 | `GET /api/install/connection-check` | 读 `.env` 验 MySQL+Redis |
|
||||
| 4 | 创建 admin | `POST /api/install/create-admin` | 需 `install_token` |
|
||||
| 5 | 完成 | `POST /api/install/lock`(兜底) | 写 `.install_locked` |
|
||||
|
||||
辅助:`GET /status`、`GET /state`(断点续装)。
|
||||
|
||||
实现文件:`server/api/install.py`、`web/app/install.html`。
|
||||
|
||||
### 3.2 步骤 3 表单(1Panel 正确填法)
|
||||
|
||||
| 字段 | MySQL | Redis |
|
||||
|------|-------|-------|
|
||||
| 主机 | `1Panel-mysql-xxxx` | `1Panel-redis-xxxx` |
|
||||
| 端口 | `3306` | `6379` |
|
||||
| 库名 | `nexus` | 库号 `0` |
|
||||
| 用户名 | `nexus` | **无**(1Panel Redis 无账号;向导仅填密码) |
|
||||
| 密码 | 1Panel「数据库」里建的 nexus 密码 | 1Panel Redis **应用参数**中的口令 |
|
||||
|
||||
### 3.3 Docker 预填来源
|
||||
|
||||
优先级(`_onepanel_service_host`):
|
||||
|
||||
1. 环境变量 `NEXUS_1PANEL_DB_HOST` / `NEXUS_1PANEL_REDIS_HOST`
|
||||
2. `/app/web/data/1panel-hosts.json`(`nx update` 写入卷)
|
||||
3. 回退 `host.docker.internal`(**1Panel 场景错误**)
|
||||
|
||||
`env-check` 在 `NEXUS_DOCKER_WRITE_ENV=1` 时返回 `docker_defaults`(主机/端口/库名,**不含** Redis 用户名)。前端会覆盖 stale 的 `127.0.0.1` / `host.docker.internal`,并隐藏 Redis 用户名字段。
|
||||
|
||||
### 3.4 连接 URL 格式(必读)
|
||||
|
||||
**MySQL**(写入 `NEXUS_DATABASE_URL`):
|
||||
|
||||
```
|
||||
mysql+aiomysql://nexus:密码@1Panel-mysql-xxxx:3306/nexus
|
||||
```
|
||||
|
||||
**Redis**(写入 `NEXUS_REDIS_URL`)— `server/api/install.py` `_build_redis_url`:
|
||||
|
||||
| 场景 | URL 格式 |
|
||||
|------|----------|
|
||||
| 1Panel(常见,推荐) | `redis://:密码@1Panel-redis-xxxx:6379/0`(向导用户名留空) |
|
||||
| 1Panel ACL default | `redis://default:密码@1Panel-redis-xxxx:6379/0` |
|
||||
| 无密码 | `redis://1Panel-redis-xxxx:6379/0` |
|
||||
|
||||
**历史 bug(807f4c2 前)**:生成 `redis://密码@host` 会把密码当成**用户名**,导致 `invalid username-password pair or user is disabled`。升级后须 `git pull` + `sync-install-wizard-to-container.sh`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 1Panel 部署流程
|
||||
|
||||
### 4.1 前置
|
||||
|
||||
1. 安装 1Panel + Docker
|
||||
2. 应用商店安装 **MySQL**、**Redis**(自动创建 `1panel-network`)
|
||||
3. 1Panel「数据库」:库 `nexus`、用户 `nexus`(仅授权 `nexus` 库)
|
||||
4. 记录 Redis 应用参数中的 **连接口令**(不是 MySQL 式账号)
|
||||
|
||||
### 4.2 安装 Nexus
|
||||
|
||||
```bash
|
||||
# 新机(curl)
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
|
||||
# 或已有仓库
|
||||
cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
```
|
||||
|
||||
完成后:`https://<域名>/app/install.html`
|
||||
|
||||
### 4.3 `nx update` 自动 sync(b25d079+)
|
||||
|
||||
`deploy/nexus-1panel.sh` 在 install/upgrade 时:
|
||||
|
||||
1. `detect-1panel-services.sh` 探测容器名
|
||||
2. 写入 `docker/.env.prod`:`NEXUS_1PANEL_*_HOST`、`NEXUS_REDIS_URL=redis://容器:6379/0`(**无密码**,密码由向导填)
|
||||
3. `write_1panel_hosts_volume` → `1panel-hosts.json`
|
||||
4. 若存在 `1panel-network`,叠加 `docker-compose.1panel.yml`
|
||||
|
||||
### 4.4 OpenResty
|
||||
|
||||
- 1Panel → 网站 → 反代 `http://127.0.0.1:8600`
|
||||
- HTTPS Let's Encrypt
|
||||
- 参考:`deploy/1panel/openresty-nexus.conf.example`(`/ws/` 超时 ≥ 3600s)
|
||||
- **禁止**脚本修改 `/opt/1panel/apps/openresty/`
|
||||
|
||||
### 4.5 资源档位
|
||||
|
||||
| Profile | 说明 |
|
||||
|---------|------|
|
||||
| `1c4g` | 小机 |
|
||||
| `2c8g` | 默认 |
|
||||
| `4c16g` | `--profile 4c16g` |
|
||||
|
||||
持久化:`docker/.host-profile`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 日常运维:`nx` 与 `update`
|
||||
|
||||
| 命令 | 等价 |
|
||||
|------|------|
|
||||
| `nx` | 交互菜单 |
|
||||
| `nx update` | `deploy/update.sh` → `nexus-1panel.sh upgrade` |
|
||||
| `nexus-update` | 同上 |
|
||||
|
||||
**upgrade 顺序**:`git fetch` → 可选 MySQL 备份 → `git reset --hard origin/main` → `sync_1panel_service_hosts` → `compose up -d --build` → `sync-install-wizard-to-container.sh` → `/health` 验收。
|
||||
|
||||
| 参数 | 含义 |
|
||||
|------|------|
|
||||
| `--check` | 仅查 Git |
|
||||
| `--no-cache` | 无缓存重建镜像 |
|
||||
| `--no-backup` | 跳过 mysqldump |
|
||||
| `--prune` | 清理悬空镜像 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 脚本速查
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [deploy/README-1panel.md](../../deploy/README-1panel.md) | 速查表 |
|
||||
| `verify-1panel-install-wizard.sh` | 服务器一键验收 |
|
||||
| `detect-1panel-services.sh` | 输出 `NEXUS_1PANEL_*_HOST` |
|
||||
| `fix-1panel-mysql-grant.sh` | 修复 MySQL 1045(`nexus@'%'`) |
|
||||
| `test-1panel-redis.sh` | **Redis 多格式认证诊断** |
|
||||
| `sync-install-wizard-to-container.sh` | 热同步 `install.html` + `install.py` |
|
||||
| `uninstall-mysql-compose.sh` | 清理旧内置 MySQL 栈 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 故障排查决策树
|
||||
|
||||
### 7.1 Redis 认证失败
|
||||
|
||||
**症状**:`invalid username-password pair` / `NOAUTH` / `wrongpass`
|
||||
|
||||
```
|
||||
1. 主机是否为 1Panel-redis-xxxx?(不是 127.0.0.1)
|
||||
2. 是否误把口令当成「root 用户名」?(1Panel Redis **无账号**,只填密码)
|
||||
3. 密码是否与 1Panel → Redis → 参数 一致?
|
||||
4. 运行 `bash deploy/test-1panel-redis.sh` 看哪种 URL 可用
|
||||
5. 运行诊断:
|
||||
cd /opt/nexus && bash deploy/test-1panel-redis.sh
|
||||
```
|
||||
|
||||
**在 Redis 容器内验证**:
|
||||
|
||||
```bash
|
||||
REDIS=$(docker ps --format '{{.Names}}' | grep -i '1Panel-redis' | head -1)
|
||||
docker exec -it "$REDIS" redis-cli -u "redis://:你的密码@127.0.0.1:6379" ping
|
||||
# 期望 PONG
|
||||
```
|
||||
|
||||
**查真实配置**(密码与 ACL):
|
||||
|
||||
```bash
|
||||
ls /opt/1panel/apps/redis/
|
||||
grep -E 'requirepass|user ' /opt/1panel/apps/redis/*/conf/redis.conf 2>/dev/null
|
||||
```
|
||||
|
||||
**修正已有 `.env`(步骤 3 已执行过)**:
|
||||
|
||||
```bash
|
||||
NEXUS=$(docker ps --format '{{.Names}}' | grep nexus-prod-nexus | head -1)
|
||||
REDIS=$(grep '^NEXUS_1PANEL_REDIS_HOST=' /opt/nexus/docker/.env.prod | cut -d= -f2)
|
||||
docker exec "$NEXUS" sed -i "s|^NEXUS_REDIS_URL=.*|NEXUS_REDIS_URL=\"redis://:你的密码@${REDIS}:6379/0\"|" /app/.env
|
||||
docker restart "$NEXUS"
|
||||
# 向导步骤 4 → 重新检测连接
|
||||
```
|
||||
|
||||
### 7.2 MySQL 1045
|
||||
|
||||
**症状**:`Access denied for user 'nexus'@'172.18.x.x'`
|
||||
|
||||
- TCP 已通,但用户仅 `nexus@localhost`
|
||||
- 修复:`bash deploy/fix-1panel-mysql-grant.sh`(需 1Panel MySQL **root** 密码,非 nexus 用户)
|
||||
|
||||
### 7.3 host.docker.internal
|
||||
|
||||
**症状**:`Can't connect to MySQL/Redis on 'host.docker.internal'`
|
||||
|
||||
```bash
|
||||
cd /opt/nexus
|
||||
bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
bash deploy/verify-1panel-install-wizard.sh
|
||||
```
|
||||
|
||||
### 7.4 系统已初始化,无法重复执行数据库安装
|
||||
|
||||
- `/app/.env` 已存在,**不要**再点步骤 3 初始化
|
||||
- 若 Redis 错:改 `.env` 中 `NEXUS_REDIS_URL`(§7.1)→ 步骤 4 重检
|
||||
- 若需完全重装:
|
||||
|
||||
```bash
|
||||
NEXUS=$(docker ps --format '{{.Names}}' | grep nexus-prod-nexus | head -1)
|
||||
docker exec "$NEXUS" rm -f /app/.env /app/.install_state.json /app/.install_locked
|
||||
docker run --rm -v nexus-prod_nexus-state:/data alpine:3.19 rm -f /data/.env
|
||||
# 确认 docker/.env.prod 中 NEXUS_INSTALL_WIZARD_PENDING=1
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
### 7.5 外网 HTTPS 失败、本机 8600 正常
|
||||
|
||||
- 配置 OpenResty 反代 + SSL
|
||||
- `curl -s http://127.0.0.1:8600/health` → `ok`
|
||||
- `curl -sk https://域名/health` → 应 `ok`
|
||||
|
||||
### 7.6 install.html 非 200
|
||||
|
||||
```bash
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
# 或 nx update --no-cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收清单
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/verify-1panel-install-wizard.sh
|
||||
```
|
||||
|
||||
关键 `[PASS]`:
|
||||
|
||||
- Git ≥ `b25d079`(1Panel sync)
|
||||
- `NEXUS_1PANEL_DB_HOST` / `NEXUS_1PANEL_REDIS_HOST`
|
||||
- Nexus 在 `1panel-network`
|
||||
- `/app/.env` 不存在(安装模式)或连接检测全绿
|
||||
- `env-check` 的 `docker_defaults.db_host` 为 `1Panel-mysql-*`
|
||||
|
||||
浏览器:步骤 3 **先检测** → 初始化 → 步骤 4 创建 admin → `/app/` 登录。
|
||||
|
||||
---
|
||||
|
||||
## 9. 提交修复索引
|
||||
|
||||
| 提交 | 主题 |
|
||||
|------|------|
|
||||
| `b25d079` | `nx update` sync 1Panel 容器名;upgrade 补 `sync_1panel_service_hosts` |
|
||||
| `d0544c9` | `fix-1panel-mysql-grant.sh`;MySQL 1045 友好错误 |
|
||||
| `da061d3` | 步骤 3 `test-credentials` 门禁 |
|
||||
| `807f4c2` | Redis URL `redis://:pass@` 修复(曾误加 root,已废弃) |
|
||||
| `3b2856f+` | 自动解析 `:pass@` / `default`;步骤 4 connection-check 可自愈 `.env` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 与功能开发 SSOT 的差异
|
||||
|
||||
| 点 | `nexus-functional-development-guide.md` §16 | 1Panel 实装 |
|
||||
|----|---------------------------------------------|-------------|
|
||||
| 步骤 2/3 | 与 init-db 合并描述 | 步骤 2 仅 env-check;步骤 3 含 test-credentials |
|
||||
| 配置三写 | 提及 `config.php` | 现为 `config.json` |
|
||||
| 生产部署 §18 | Supervisor + 宝塔 | 以 `nexus-1panel.sh` / `nx` 为主 |
|
||||
| Redis 主机 | 泛化为 localhost | **1Panel 必须容器名 + 仅密码(`:pass@`)** |
|
||||
|
||||
---
|
||||
|
||||
## 11. 相关文档
|
||||
|
||||
- [nexus-functional-development-guide.md](nexus-functional-development-guide.md) — 全栈功能 SSOT
|
||||
- [nexus-functional-development-guide-appendix.md](nexus-functional-development-guide-appendix.md) — API 附录
|
||||
- [production-verification-checklist.md](production-verification-checklist.md) — L5
|
||||
- [linux-dev-paths.md](linux-dev-paths.md) — 本地路径
|
||||
- [../changelog/](../changelog/) — 按日变更记录
|
||||
|
||||
---
|
||||
|
||||
*本文档随 1Panel 部署实践更新;代码行为以 `server/api/install.py` 与 `deploy/nexus-1panel.sh` 为准。*
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate per-host Nexus secrets (install / Docker).
|
||||
|
||||
Each fresh install must get unique MYSQL_* and NEXUS_* keys. Matches install wizard:
|
||||
Fresh install generates unique NEXUS_* keys only (MySQL is self-hosted).
|
||||
Migration (--from-env) may still import legacy MYSQL_* from old .env.prod.
|
||||
|
||||
Matches install wizard:
|
||||
SECRET_KEY = token_hex(32)
|
||||
API_KEY = token_hex(16)
|
||||
ENCRYPTION_KEY = urlsafe_b64encode(token_bytes(32))
|
||||
@@ -25,14 +28,16 @@ if str(ROOT) not in sys.path:
|
||||
from server.utils.text_io import read_utf8_lf, write_utf8_lf # noqa: E402
|
||||
|
||||
|
||||
def generate_nexus_secrets() -> dict[str, str]:
|
||||
return {
|
||||
"MYSQL_ROOT_PASSWORD": secrets.token_urlsafe(24),
|
||||
"MYSQL_PASSWORD": secrets.token_urlsafe(24),
|
||||
def generate_nexus_secrets(*, include_mysql: bool = False) -> dict[str, str]:
|
||||
out: dict[str, str] = {
|
||||
"NEXUS_SECRET_KEY": secrets.token_hex(32),
|
||||
"NEXUS_API_KEY": secrets.token_hex(16),
|
||||
"NEXUS_ENCRYPTION_KEY": base64.urlsafe_b64encode(secrets.token_bytes(32)).decode(),
|
||||
}
|
||||
if include_mysql:
|
||||
out["MYSQL_ROOT_PASSWORD"] = secrets.token_urlsafe(24)
|
||||
out["MYSQL_PASSWORD"] = secrets.token_urlsafe(24)
|
||||
return out
|
||||
|
||||
|
||||
def parse_env_file(path: Path) -> dict[str, str]:
|
||||
@@ -51,13 +56,13 @@ def parse_env_file(path: Path) -> dict[str, str]:
|
||||
def import_secrets_from_env(path: Path) -> dict[str, str]:
|
||||
"""Import NEXUS + MySQL passwords from an existing .env (migration)."""
|
||||
raw = parse_env_file(path)
|
||||
out = generate_nexus_secrets()
|
||||
out = generate_nexus_secrets(include_mysql=False)
|
||||
for key in (
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
"NEXUS_SECRET_KEY",
|
||||
"NEXUS_API_KEY",
|
||||
"NEXUS_ENCRYPTION_KEY",
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
):
|
||||
val = raw.get(key, "").strip()
|
||||
if val:
|
||||
@@ -155,13 +160,13 @@ def write_secrets_backup(path: Path, secrets_map: dict[str, str], *, domain: str
|
||||
"",
|
||||
]
|
||||
for key in (
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
"NEXUS_SECRET_KEY",
|
||||
"NEXUS_API_KEY",
|
||||
"NEXUS_ENCRYPTION_KEY",
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
):
|
||||
if key in secrets_map:
|
||||
if secrets_map.get(key):
|
||||
lines.append(f"{key}={secrets_map[key]}")
|
||||
write_utf8_lf(path, "\n".join(lines) + "\n")
|
||||
path.chmod(0o600)
|
||||
@@ -212,10 +217,10 @@ def main() -> None:
|
||||
secrets_map = import_secrets_from_env(args.from_env)
|
||||
wizard = False
|
||||
elif args.fresh:
|
||||
secrets_map = generate_nexus_secrets()
|
||||
secrets_map = generate_nexus_secrets(include_mysql=False)
|
||||
wizard = bool(args.wizard_pending)
|
||||
else:
|
||||
secrets_map = generate_nexus_secrets()
|
||||
secrets_map = generate_nexus_secrets(include_mysql=False)
|
||||
wizard = bool(args.wizard_pending)
|
||||
|
||||
write_env_prod(
|
||||
|
||||
+457
-43
@@ -12,7 +12,7 @@ import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote_plus, urlparse
|
||||
from urllib.parse import quote_plus, unquote_plus, urlparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -33,6 +33,7 @@ ENV_FILE = ROOT_DIR / ".env"
|
||||
INSTALL_LOCK = ROOT_DIR / ".install_locked"
|
||||
CONFIG_DIR = ROOT_DIR / "web" / "data"
|
||||
CONFIG_JSON = CONFIG_DIR / "config.json"
|
||||
ONEPANEL_HOSTS_JSON = CONFIG_DIR / "1panel-hosts.json"
|
||||
STATE_FILE = ROOT_DIR / ".install_state.json"
|
||||
|
||||
|
||||
@@ -61,6 +62,7 @@ class InitDbRequest(BaseModel):
|
||||
redis_host: str = "127.0.0.1"
|
||||
redis_port: str = "6379"
|
||||
redis_db: str = "0"
|
||||
redis_user: str = ""
|
||||
redis_pass: str = ""
|
||||
api_port: str = "8600"
|
||||
timezone: str = "Asia/Shanghai"
|
||||
@@ -91,15 +93,48 @@ def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
|
||||
|
||||
|
||||
def _build_redis_url(req: InitDbRequest) -> str:
|
||||
url = "redis://"
|
||||
if req.redis_pass:
|
||||
url += f"{quote_plus(req.redis_pass)}@"
|
||||
url += f"{req.redis_host}:{req.redis_port}/{req.redis_db}"
|
||||
return url
|
||||
"""Build redis:// URL. Password-only auth must use redis://:pass@host (leading colon)."""
|
||||
user = (req.redis_user or "").strip()
|
||||
password = (req.redis_pass or "").strip()
|
||||
auth = ""
|
||||
if user and password:
|
||||
auth = f"{quote_plus(user)}:{quote_plus(password)}@"
|
||||
elif password:
|
||||
auth = f":{quote_plus(password)}@"
|
||||
elif user:
|
||||
auth = f"{quote_plus(user)}@"
|
||||
return f"redis://{auth}{req.redis_host}:{req.redis_port}/{req.redis_db}"
|
||||
|
||||
|
||||
def _read_1panel_hosts_file() -> dict[str, str]:
|
||||
if not ONEPANEL_HOSTS_JSON.is_file():
|
||||
return {}
|
||||
import json
|
||||
|
||||
try:
|
||||
raw = json.loads(read_utf8_text(ONEPANEL_HOSTS_JSON))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
return {str(k): str(v) for k, v in raw.items() if v}
|
||||
|
||||
|
||||
def _onepanel_service_host(kind: str) -> str | None:
|
||||
"""1Panel App Store container name (env → web/data/1panel-hosts.json)."""
|
||||
val = os.environ.get(f"NEXUS_1PANEL_{kind}_HOST", "").strip()
|
||||
if val:
|
||||
return val
|
||||
file_key = "db_host" if kind == "DB" else "redis_host"
|
||||
return _read_1panel_hosts_file().get(file_key) or None
|
||||
|
||||
|
||||
def _resolve_redis_probe() -> tuple[str, int, str | None]:
|
||||
"""Redis target for install wizard health check (Docker uses redis:6379, not host loopback)."""
|
||||
"""Redis target for install wizard (1Panel: container name on 1panel-network)."""
|
||||
onepanel = _onepanel_service_host("REDIS")
|
||||
if onepanel:
|
||||
return onepanel, 6379, None
|
||||
|
||||
url = os.environ.get("NEXUS_REDIS_URL", "").strip()
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
@@ -112,16 +147,327 @@ def _resolve_redis_probe() -> tuple[str, int, str | None]:
|
||||
return host, port, None
|
||||
|
||||
|
||||
def _service_tcp_targets(port: int, onepanel_kind: str) -> list[tuple[str, int]]:
|
||||
"""Probe order: 1Panel container name → host.docker.internal → loopback."""
|
||||
targets: list[tuple[str, int]] = []
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
host = _onepanel_service_host(onepanel_kind)
|
||||
if host:
|
||||
targets.append((host, port))
|
||||
targets.append(("host.docker.internal", port))
|
||||
targets.append(("127.0.0.1", port))
|
||||
seen: set[tuple[str, int]] = set()
|
||||
ordered: list[tuple[str, int]] = []
|
||||
for item in targets:
|
||||
if item in seen:
|
||||
continue
|
||||
seen.add(item)
|
||||
ordered.append(item)
|
||||
return ordered
|
||||
|
||||
|
||||
def _probe_tcp_service(port: int, onepanel_kind: str) -> tuple[bool, str]:
|
||||
import socket
|
||||
|
||||
for host, p in _service_tcp_targets(port, onepanel_kind):
|
||||
try:
|
||||
with socket.create_connection((host, p), timeout=2.0):
|
||||
return True, f"✓ 已安装({host}:{p} 可访问)"
|
||||
except OSError:
|
||||
continue
|
||||
hint_host = _onepanel_service_host(onepanel_kind)
|
||||
if not hint_host and os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
hint_host = "host.docker.internal"
|
||||
if not hint_host:
|
||||
hint_host = "127.0.0.1"
|
||||
return False, f"✗ 未检测到服务({hint_host}:{port})"
|
||||
|
||||
|
||||
def _probe_redis_installed() -> tuple[bool, str]:
|
||||
"""Step 2: TCP probe only — Redis 是否可访问(不验证密码/库号)。"""
|
||||
return _probe_tcp_service(6379, "REDIS")
|
||||
|
||||
|
||||
def _probe_mysql_installed() -> tuple[bool, str]:
|
||||
"""Step 2: TCP probe — MySQL 是否可访问(不验证账号密码)。"""
|
||||
return _probe_tcp_service(3306, "DB")
|
||||
|
||||
|
||||
def _service_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool:
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _mysql_auth_error_detail(db_user: str) -> str:
|
||||
"""1045 Access denied — common on 1Panel when user is only nexus@localhost."""
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
return (
|
||||
f"MySQL 拒绝用户 {db_user!r} 的密码或主机权限。"
|
||||
"请核对库名/用户名/密码,并确认该用户允许从应用主机连接(非仅 localhost)。"
|
||||
)
|
||||
return (
|
||||
f"MySQL 拒绝用户 {db_user!r}(错误 1045)。Nexus 在 Docker 内通过 1panel-network 连接,"
|
||||
"来源 IP 为容器网段(如 172.18.x.x),不是 localhost。\n"
|
||||
"请在 1Panel MySQL 中确保:① 库 nexus、用户 nexus 已创建 ② 密码与向导一致 "
|
||||
"③ 存在 nexus@'%' 或允许 Docker 网段(仅 nexus@localhost 会失败)。\n"
|
||||
"服务器执行: cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh"
|
||||
)
|
||||
|
||||
|
||||
def _mysql_connect_error_detail(host: str, port: int) -> str:
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
return ""
|
||||
onepanel_db = _onepanel_service_host("DB")
|
||||
if onepanel_db and host == onepanel_db:
|
||||
return (
|
||||
f"无法连接 1Panel MySQL 容器 {host}:{port}。"
|
||||
"请确认:① MySQL 容器运行中 ② Nexus 已接入 1panel-network(服务器执行 nx update)"
|
||||
"③ 已在 1Panel 创建库 nexus 与用户 nexus。"
|
||||
)
|
||||
if host in ("host.docker.internal", "172.17.0.1"):
|
||||
example = onepanel_db or "1Panel-mysql-xxxx"
|
||||
return (
|
||||
f"无法通过 {host} 连接 MySQL。"
|
||||
"1Panel 应用商店 MySQL 在 1panel-network 内,容器间应使用 MySQL 容器名"
|
||||
f"(如 {example}),而非 host.docker.internal。"
|
||||
"在服务器执行 nx update 可自动接入网络并预填容器名。"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _redis_auth_error_detail() -> str:
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
return (
|
||||
"Redis 认证失败。1Panel:主机 1Panel-redis-xxxx;密码为应用参数;"
|
||||
"用户名通常留空(自动尝试 :pass@)或填 default。勿填 root(多数实例不支持)。"
|
||||
"诊断: bash deploy/test-1panel-redis.sh"
|
||||
)
|
||||
return "Redis 密码错误或需要 AUTH;请核对用户名/密码(无密码则留空)。"
|
||||
|
||||
|
||||
def _redis_connect_error_detail(host: str, port: int) -> str:
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
return ""
|
||||
onepanel_redis = _onepanel_service_host("REDIS")
|
||||
if onepanel_redis and host == onepanel_redis:
|
||||
return (
|
||||
f"无法连接 1Panel Redis 容器 {host}:{port}。"
|
||||
"请确认:① Redis 容器运行中 ② Nexus 已接入 1panel-network(服务器执行 nx update)"
|
||||
"③ 若 1Panel Redis 设置了密码,请在步骤 3 填写 redis_pass。"
|
||||
)
|
||||
if host in ("host.docker.internal", "172.17.0.1"):
|
||||
example = onepanel_redis or "1Panel-redis-xxxx"
|
||||
return (
|
||||
f"无法通过 {host} 连接 Redis。"
|
||||
"1Panel 应用商店 Redis 在 1panel-network 内,容器间应使用 Redis 容器名"
|
||||
f"(如 {example}),而非 host.docker.internal。"
|
||||
"在服务器执行 nx update 可自动接入并预填容器名。"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_dotenv(path: Path) -> dict[str, str]:
|
||||
data: dict[str, str] = {}
|
||||
if not path.is_file():
|
||||
return data
|
||||
for line in read_utf8_text(path).splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, raw = line.partition("=")
|
||||
val = raw.strip()
|
||||
if len(val) >= 2 and val[0] == val[-1] == '"':
|
||||
val = val[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
||||
data[key.strip()] = val
|
||||
return data
|
||||
|
||||
|
||||
async def _test_mysql_url(db_url: str) -> tuple[bool, str]:
|
||||
if not db_url:
|
||||
return False, "✗ 未配置 NEXUS_DATABASE_URL"
|
||||
try:
|
||||
patch_aiomysql_do_ping()
|
||||
engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300)
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
await engine.dispose()
|
||||
return True, "✓ MySQL 连接正常"
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
if "1045" in err or "Access denied" in err:
|
||||
return False, _mysql_auth_error_detail(
|
||||
urlparse(db_url).username or "nexus"
|
||||
)
|
||||
return False, f"✗ MySQL 连接失败: {e}"
|
||||
|
||||
|
||||
def _test_redis_url(redis_url: str) -> tuple[bool, str]:
|
||||
if not redis_url:
|
||||
return False, "✗ 未配置 NEXUS_REDIS_URL"
|
||||
parsed = urlparse(redis_url)
|
||||
host = parsed.hostname or "127.0.0.1"
|
||||
port = parsed.port or 6379
|
||||
if not _service_tcp_reachable(host, port):
|
||||
detail = _redis_connect_error_detail(host, port)
|
||||
return False, detail or f"✗ Redis TCP 不通({host}:{port})"
|
||||
try:
|
||||
import redis as rlib
|
||||
|
||||
client = rlib.Redis.from_url(redis_url, socket_timeout=3.0)
|
||||
client.ping()
|
||||
return True, "✓ Redis 连接正常"
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
if "Connection refused" in err or "Error 111" in err:
|
||||
detail = _redis_connect_error_detail(host, port)
|
||||
if detail:
|
||||
return False, detail
|
||||
lowered = err.lower()
|
||||
if (
|
||||
"invalid username-password" in lowered
|
||||
or "wrongpass" in lowered
|
||||
or "noauth" in lowered
|
||||
or "authentication required" in lowered
|
||||
):
|
||||
return False, _redis_auth_error_detail()
|
||||
return False, f"✗ Redis 连接失败: {e}"
|
||||
|
||||
|
||||
def _is_docker_install_mode() -> bool:
|
||||
return os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
|
||||
|
||||
|
||||
def _sanitize_redis_request(req: InitDbRequest) -> InitDbRequest:
|
||||
"""1Panel/Docker: Redis has no account — ignore username (incl. mistaken root)."""
|
||||
user = (req.redis_user or "").strip()
|
||||
if _is_docker_install_mode():
|
||||
if user:
|
||||
return req.model_copy(update={"redis_user": ""})
|
||||
return req
|
||||
if user.lower() == "root":
|
||||
return req.model_copy(update={"redis_user": ""})
|
||||
return req
|
||||
|
||||
|
||||
def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
|
||||
"""Auth variants: 1Panel tries :pass@ then default only (no root)."""
|
||||
password = (req.redis_pass or "").strip()
|
||||
if not password:
|
||||
return [("无密码", _build_redis_url(req))]
|
||||
|
||||
seen: set[str] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
|
||||
def add(label: str, candidate: InitDbRequest) -> None:
|
||||
url = _build_redis_url(candidate)
|
||||
if url in seen:
|
||||
return
|
||||
seen.add(url)
|
||||
out.append((label, url))
|
||||
|
||||
if _is_docker_install_mode():
|
||||
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
|
||||
add("用户 default", req.model_copy(update={"redis_user": "default"}))
|
||||
return out
|
||||
|
||||
user = (req.redis_user or "").strip()
|
||||
if user and user.lower() != "root":
|
||||
add(f"用户 {user}", req)
|
||||
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
|
||||
if user.lower() != "default":
|
||||
add("用户 default", req.model_copy(update={"redis_user": "default"}))
|
||||
return out
|
||||
|
||||
|
||||
def _init_db_request_from_redis_url(redis_url: str) -> InitDbRequest | None:
|
||||
"""Parse NEXUS_REDIS_URL into InitDbRequest fields for re-probe / repair."""
|
||||
if not redis_url.strip():
|
||||
return None
|
||||
parsed = urlparse(redis_url)
|
||||
if not parsed.hostname:
|
||||
return None
|
||||
user = unquote_plus(parsed.username or "")
|
||||
password = unquote_plus(parsed.password or "")
|
||||
if not password and user:
|
||||
# Legacy bug: redis://pass@host (password stored as username)
|
||||
password = user
|
||||
user = ""
|
||||
db = (parsed.path or "/0").lstrip("/") or "0"
|
||||
return InitDbRequest(
|
||||
db_pass=".",
|
||||
redis_host=parsed.hostname,
|
||||
redis_port=str(parsed.port or 6379),
|
||||
redis_db=db,
|
||||
redis_user=user,
|
||||
redis_pass=password,
|
||||
)
|
||||
|
||||
|
||||
def _mask_redis_url(redis_url: str) -> str:
|
||||
parsed = urlparse(redis_url)
|
||||
if not parsed.password:
|
||||
return redis_url
|
||||
host = parsed.hostname or ""
|
||||
port = parsed.port
|
||||
user = parsed.username or ""
|
||||
if user:
|
||||
netloc = f"{user}:***@{host}"
|
||||
else:
|
||||
netloc = f":***@{host}"
|
||||
if port:
|
||||
netloc = f"{netloc}:{port}"
|
||||
path = parsed.path or "/0"
|
||||
return f"redis://{netloc}{path}"
|
||||
|
||||
|
||||
def _patch_env_redis_url(new_url: str) -> None:
|
||||
"""Update only NEXUS_REDIS_URL in existing .env (install step 4 repair)."""
|
||||
if not ENV_FILE.is_file():
|
||||
return
|
||||
lines = read_utf8_text(ENV_FILE).splitlines()
|
||||
new_line = f"NEXUS_REDIS_URL={_escape_env_value(new_url)}"
|
||||
replaced = False
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
if line.strip().startswith("NEXUS_REDIS_URL="):
|
||||
out.append(new_line)
|
||||
replaced = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not replaced:
|
||||
out.append(new_line)
|
||||
write_utf8_lf(ENV_FILE, "\n".join(out) + "\n")
|
||||
logger.info("Patched NEXUS_REDIS_URL in %s", ENV_FILE)
|
||||
|
||||
|
||||
def _resolve_redis_url(req: InitDbRequest) -> tuple[bool, str, str | None]:
|
||||
"""Pick first working Redis URL among 1Panel-compatible auth variants."""
|
||||
last_msg = "✗ 未配置 Redis"
|
||||
for label, url in _redis_url_candidates(req):
|
||||
ok, msg = _test_redis_url(url)
|
||||
last_msg = msg
|
||||
if ok:
|
||||
return True, f"✓ Redis 连接正常({label})", url
|
||||
return False, last_msg, None
|
||||
|
||||
|
||||
def _docker_wizard_defaults() -> dict[str, str] | None:
|
||||
"""Prefill install step 3 when running inside docker-compose.prod.yml."""
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1" and not os.environ.get("REDIS_HOST"):
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
return None
|
||||
db_host = _onepanel_service_host("DB") or "host.docker.internal"
|
||||
redis_host = _onepanel_service_host("REDIS") or "host.docker.internal"
|
||||
return {
|
||||
"db_host": os.environ.get("MYSQL_HOST", "mysql"),
|
||||
"db_port": os.environ.get("MYSQL_PORT", "3306"),
|
||||
"db_host": db_host,
|
||||
"db_port": "3306",
|
||||
"db_name": "nexus",
|
||||
"db_user": "nexus",
|
||||
"redis_host": "host.docker.internal",
|
||||
"redis_host": redis_host,
|
||||
"redis_port": "6379",
|
||||
"redis_db": "0",
|
||||
}
|
||||
@@ -442,40 +788,24 @@ async def env_check():
|
||||
"pass": False,
|
||||
})
|
||||
|
||||
# Redis — 用户自行安装(1Panel/宿主机),不阻塞安装向导
|
||||
redis_ok = False
|
||||
redis_msg = "未检测"
|
||||
rh, rp, rpass = _resolve_redis_probe()
|
||||
redis_label = f"{rh}:{rp}"
|
||||
try:
|
||||
import redis as rlib
|
||||
try:
|
||||
r = rlib.Redis(
|
||||
host=rh,
|
||||
port=rp,
|
||||
password=rpass or None,
|
||||
socket_timeout=0.5,
|
||||
)
|
||||
r.ping()
|
||||
redis_ok = True
|
||||
redis_msg = f"✓ 已连接 {redis_label}"
|
||||
except Exception as e:
|
||||
redis_msg = f"未连接 {redis_label}(可继续,步骤3配置或稍后安装 Redis)— {e}"
|
||||
except ImportError:
|
||||
redis_msg = "redis 库未安装(可继续)"
|
||||
# Redis — 步骤2仅检测宿主机是否已安装(TCP);步骤4再验证连接
|
||||
redis_installed, redis_msg = _probe_redis_installed()
|
||||
checks.append({
|
||||
"name": "Redis",
|
||||
"required": "自行安装(步骤3配置)",
|
||||
"name": "Redis(宿主机)",
|
||||
"required": "建议已安装",
|
||||
"current": redis_msg,
|
||||
"pass": True,
|
||||
"pass": redis_installed,
|
||||
"blocks_wizard": False,
|
||||
})
|
||||
|
||||
# MySQL database — user configures in step 3
|
||||
# MySQL — 步骤2 TCP 检测;步骤4再验证账号密码
|
||||
mysql_installed, mysql_msg = _probe_mysql_installed()
|
||||
checks.append({
|
||||
"name": "MySQL 数据库",
|
||||
"required": "步骤3配置",
|
||||
"current": "— 请在步骤3填写数据库连接信息",
|
||||
"pass": True,
|
||||
"name": "MySQL(宿主机)",
|
||||
"required": "建议已安装",
|
||||
"current": mysql_msg,
|
||||
"pass": mysql_installed,
|
||||
"blocks_wizard": False,
|
||||
})
|
||||
|
||||
# Write permissions
|
||||
@@ -495,7 +825,7 @@ async def env_check():
|
||||
"pass": root_writable,
|
||||
})
|
||||
|
||||
all_pass = all(c["pass"] for c in checks)
|
||||
all_pass = all(c["pass"] for c in checks if c.get("blocks_wizard", True))
|
||||
payload: dict = {"checks": checks, "all_pass": all_pass}
|
||||
defaults = _docker_wizard_defaults()
|
||||
if defaults:
|
||||
@@ -503,6 +833,74 @@ async def env_check():
|
||||
return payload
|
||||
|
||||
|
||||
async def _credential_checks(req: InitDbRequest) -> tuple[list[dict], bool, str | None]:
|
||||
"""Verify MySQL + Redis with credentials from install step 3 (TCP + AUTH)."""
|
||||
req = _sanitize_redis_request(req)
|
||||
db_url = _make_db_url(req)
|
||||
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
|
||||
redis_ok, redis_msg, redis_url = _resolve_redis_url(req)
|
||||
checks = [
|
||||
{"name": "MySQL", "required": "可连接", "current": mysql_msg, "pass": mysql_ok},
|
||||
{"name": "Redis", "required": "可连接", "current": redis_msg, "pass": redis_ok},
|
||||
]
|
||||
return checks, mysql_ok and redis_ok, redis_url if redis_ok else None
|
||||
|
||||
|
||||
@router.post("/test-credentials")
|
||||
async def test_credentials(req: InitDbRequest):
|
||||
"""Step 3: Test MySQL/Redis account password before init-db writes .env."""
|
||||
_reject_if_locked()
|
||||
if _has_env():
|
||||
raise HTTPException(
|
||||
400,
|
||||
"系统已初始化,无法重复检测。请继续步骤 4 创建管理员,或修正 .env 后使用连接检测。",
|
||||
)
|
||||
checks, all_pass, redis_url = await _credential_checks(req)
|
||||
payload: dict = {"checks": checks, "all_pass": all_pass}
|
||||
if redis_url:
|
||||
payload["resolved_redis_url"] = _mask_redis_url(redis_url)
|
||||
return payload
|
||||
|
||||
|
||||
@router.get("/connection-check")
|
||||
async def connection_check():
|
||||
"""Step 4: Verify MySQL + Redis using .env written in step 3."""
|
||||
_reject_if_locked()
|
||||
if not _has_env():
|
||||
raise HTTPException(400, "请先完成步骤3:数据库初始化")
|
||||
|
||||
env = _parse_dotenv(ENV_FILE)
|
||||
db_url = env.get("NEXUS_DATABASE_URL", "")
|
||||
redis_url = env.get("NEXUS_REDIS_URL", "")
|
||||
|
||||
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
|
||||
redis_ok, redis_msg = _test_redis_url(redis_url)
|
||||
redis_repaired = False
|
||||
resolved_display: str | None = None
|
||||
|
||||
if not redis_ok and redis_url:
|
||||
req_from_env = _init_db_request_from_redis_url(redis_url)
|
||||
if req_from_env:
|
||||
ok, msg, resolved = _resolve_redis_url(_sanitize_redis_request(req_from_env))
|
||||
if ok and resolved:
|
||||
if resolved != redis_url and not _is_locked():
|
||||
_patch_env_redis_url(resolved)
|
||||
redis_repaired = True
|
||||
redis_ok, redis_msg = True, msg
|
||||
resolved_display = _mask_redis_url(resolved)
|
||||
|
||||
checks = [
|
||||
{"name": "MySQL", "required": "可连接", "current": mysql_msg, "pass": mysql_ok},
|
||||
{"name": "Redis", "required": "可连接", "current": redis_msg, "pass": redis_ok},
|
||||
]
|
||||
payload: dict = {"checks": checks, "all_pass": mysql_ok and redis_ok}
|
||||
if redis_repaired:
|
||||
payload["redis_repaired"] = True
|
||||
if resolved_display:
|
||||
payload["resolved_redis_url"] = resolved_display
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/init-db")
|
||||
async def init_db(req: InitDbRequest):
|
||||
"""Step 3: Connect to MySQL, create tables, write configs."""
|
||||
@@ -510,9 +908,18 @@ async def init_db(req: InitDbRequest):
|
||||
_reject_if_env_exists()
|
||||
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
|
||||
checks, cred_ok, redis_url = await _credential_checks(req)
|
||||
if not cred_ok:
|
||||
failed = next((c for c in checks if not c["pass"]), checks[0])
|
||||
raise HTTPException(
|
||||
400,
|
||||
failed.get("current") or "MySQL 或 Redis 连接未通过,请先检测账号密码",
|
||||
)
|
||||
if not redis_url:
|
||||
redis_url = _build_redis_url(req)
|
||||
|
||||
try:
|
||||
patch_aiomysql_do_ping()
|
||||
engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300)
|
||||
@@ -602,6 +1009,13 @@ async def init_db(req: InitDbRequest):
|
||||
|
||||
except Exception as e:
|
||||
await engine.dispose()
|
||||
err = str(e)
|
||||
if "1045" in err or "Access denied" in err:
|
||||
raise HTTPException(400, _mysql_auth_error_detail(req.db_user)) from e
|
||||
if "2003" in err or "Can't connect to MySQL" in err:
|
||||
extra = _mysql_connect_error_detail(req.db_host, db_port)
|
||||
if extra:
|
||||
raise HTTPException(400, extra) from e
|
||||
raise HTTPException(400, f"数据库初始化失败: {e}") from e
|
||||
|
||||
await engine.dispose()
|
||||
@@ -633,7 +1047,7 @@ async def init_db(req: InitDbRequest):
|
||||
"site_url": site_url,
|
||||
"api_port": req.api_port,
|
||||
"guardian_results": guardian_results,
|
||||
"step": 3,
|
||||
"step": 4,
|
||||
}
|
||||
write_utf8_lf(STATE_FILE, json.dumps(state, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
@@ -174,6 +174,155 @@ def test_vendor_manifest_lists_core_assets():
|
||||
assert (manifest_path.parent / name).is_file()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_test_credentials_rejects_when_env_exists(monkeypatch, tmp_path):
|
||||
from fastapi import HTTPException
|
||||
from server.api import install as install_api
|
||||
from server.api.install import InitDbRequest, test_credentials
|
||||
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("NEXUS_SECRET_KEY=test\n", encoding="utf-8")
|
||||
monkeypatch.setattr(install_api, "ENV_FILE", env_file)
|
||||
monkeypatch.setattr(install_api, "INSTALL_LOCK", tmp_path / ".install_locked")
|
||||
|
||||
req = InitDbRequest(db_pass="secret")
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await test_credentials(req)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_test_credentials_all_pass(monkeypatch, tmp_path):
|
||||
from server.api import install as install_api
|
||||
from server.api.install import InitDbRequest, test_credentials
|
||||
|
||||
monkeypatch.setattr(install_api, "ENV_FILE", tmp_path / ".env")
|
||||
monkeypatch.setattr(install_api, "INSTALL_LOCK", tmp_path / ".install_locked")
|
||||
|
||||
async def _mysql_ok(_url):
|
||||
return True, "✓ MySQL 连接正常"
|
||||
|
||||
monkeypatch.setattr(install_api, "_test_mysql_url", _mysql_ok)
|
||||
monkeypatch.setattr(install_api, "_test_redis_url", lambda _url: (True, "✓ Redis 连接正常"))
|
||||
|
||||
req = InitDbRequest(db_pass="secret")
|
||||
out = await test_credentials(req)
|
||||
assert out["all_pass"] is True
|
||||
assert len(out["checks"]) == 2
|
||||
|
||||
|
||||
def test_build_redis_url_password_with_leading_colon():
|
||||
from server.api.install import InitDbRequest, _build_redis_url
|
||||
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_pass="redis_WP3NyN",
|
||||
)
|
||||
assert _build_redis_url(req) == "redis://:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||
|
||||
|
||||
def test_build_redis_url_with_default_user():
|
||||
from server.api.install import InitDbRequest, _build_redis_url
|
||||
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_user="default",
|
||||
redis_pass="redis_WP3NyN",
|
||||
)
|
||||
assert _build_redis_url(req) == "redis://default:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||
|
||||
|
||||
def test_resolve_redis_url_falls_back_to_colon_pass(monkeypatch):
|
||||
from server.api.install import InitDbRequest, _resolve_redis_url, _sanitize_redis_request
|
||||
|
||||
monkeypatch.setenv("NEXUS_DOCKER_WRITE_ENV", "1")
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_user="root",
|
||||
redis_pass="secret",
|
||||
)
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_test(url: str) -> tuple[bool, str]:
|
||||
calls.append(url)
|
||||
return url.startswith("redis://:secret@"), "ok" if url.startswith("redis://:secret@") else "fail"
|
||||
|
||||
monkeypatch.setattr("server.api.install._test_redis_url", fake_test)
|
||||
ok, msg, url = _resolve_redis_url(_sanitize_redis_request(req))
|
||||
assert ok is True
|
||||
assert url == "redis://:secret@1Panel-redis-x:6379/0"
|
||||
assert "仅密码" in msg
|
||||
assert not any(u.startswith("redis://root:") for u in calls)
|
||||
|
||||
|
||||
def test_redis_url_candidates_never_tries_root_in_docker_mode(monkeypatch):
|
||||
from server.api.install import InitDbRequest, _redis_url_candidates, _sanitize_redis_request
|
||||
|
||||
monkeypatch.setenv("NEXUS_DOCKER_WRITE_ENV", "1")
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_user="root",
|
||||
redis_pass="secret",
|
||||
)
|
||||
labels = [label for label, url in _redis_url_candidates(_sanitize_redis_request(req))]
|
||||
urls = [url for _, url in _redis_url_candidates(_sanitize_redis_request(req))]
|
||||
assert "root" not in " ".join(labels).lower()
|
||||
assert not any("root:" in u for u in urls)
|
||||
|
||||
|
||||
def test_init_db_request_from_redis_url_legacy_and_colon_pass():
|
||||
from server.api.install import _init_db_request_from_redis_url
|
||||
|
||||
legacy = _init_db_request_from_redis_url("redis://badpass@1Panel-redis-x:6379/0")
|
||||
assert legacy is not None
|
||||
assert legacy.redis_pass == "badpass"
|
||||
assert legacy.redis_user == ""
|
||||
|
||||
colon = _init_db_request_from_redis_url("redis://:secret@1Panel-redis-x:6379/0")
|
||||
assert colon is not None
|
||||
assert colon.redis_pass == "secret"
|
||||
assert colon.redis_user == ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_check_repairs_bad_redis_url(monkeypatch, tmp_path):
|
||||
from server.api import install as install_api
|
||||
from server.api.install import connection_check
|
||||
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text(
|
||||
'NEXUS_DATABASE_URL="mysql+aiomysql://n:pass@h:3306/n"\n'
|
||||
'NEXUS_REDIS_URL="redis://root:secret@1Panel-redis-x:6379/0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(install_api, "ENV_FILE", env_file)
|
||||
monkeypatch.setattr(install_api, "INSTALL_LOCK", tmp_path / ".install_locked")
|
||||
|
||||
async def _mysql_ok(_url):
|
||||
return True, "✓ MySQL"
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_redis(url: str) -> tuple[bool, str]:
|
||||
calls.append(url)
|
||||
if url.startswith("redis://:secret@"):
|
||||
return True, "✓ Redis 连接正常(仅密码 (:pass@))"
|
||||
return False, "auth fail"
|
||||
|
||||
monkeypatch.setattr(install_api, "_test_mysql_url", _mysql_ok)
|
||||
monkeypatch.setattr(install_api, "_test_redis_url", fake_redis)
|
||||
monkeypatch.setenv("NEXUS_DOCKER_WRITE_ENV", "1")
|
||||
|
||||
out = await connection_check()
|
||||
assert out["all_pass"] is True
|
||||
assert out.get("redis_repaired") is True
|
||||
assert "redis://:secret@" in env_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_install_reject_repeat_init_when_env_exists(monkeypatch, tmp_path):
|
||||
from fastapi import HTTPException
|
||||
from server.api import install as install_api
|
||||
|
||||
+207
-21
@@ -121,14 +121,17 @@
|
||||
</div>
|
||||
|
||||
<div x-show="!envLoading && envAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-4 text-sm">
|
||||
✓ 所有环境检测通过,可以继续安装。
|
||||
✓ 环境检测通过。Redis 仅检测是否已安装;MySQL/Redis <b>连接</b>在步骤 4 验证。
|
||||
</div>
|
||||
<div x-show="!envLoading && !envAllPass && envChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-sm">
|
||||
部分环境检测未通过,请先解决上述问题。
|
||||
部分必检项未通过,请先解决(Redis 未安装可先继续,但步骤 4 须连通)。
|
||||
</div>
|
||||
|
||||
<div x-show="!envLoading" class="flex gap-3 mt-4">
|
||||
<button @click="step = 3" class="bg-blue-500 hover:bg-blue-600 text-white px-5 py-2.5 rounded-lg font-semibold transition">
|
||||
<button @click="step = 3"
|
||||
:disabled="!envAllPass"
|
||||
:class="envAllPass ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-300 cursor-not-allowed'"
|
||||
class="text-white px-5 py-2.5 rounded-lg font-semibold transition">
|
||||
下一步:数据库配置 →
|
||||
</button>
|
||||
<button @click="checkEnv()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">
|
||||
@@ -155,7 +158,7 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">主机</label>
|
||||
<input x-model="form.db_host" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
<input x-model="form.db_host" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">端口</label>
|
||||
@@ -165,16 +168,16 @@
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">数据库名</label>
|
||||
<input x-model="form.db_name" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
<input x-model="form.db_name" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">用户名</label>
|
||||
<input x-model="form.db_user" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
<input x-model="form.db_user" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">密码</label>
|
||||
<input x-model="form.db_pass" type="password" required placeholder="数据库密码"
|
||||
<input x-model="form.db_pass" @input="invalidateCredCheck()" type="password" required placeholder="数据库密码"
|
||||
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,27 +185,40 @@
|
||||
<!-- Redis -->
|
||||
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
||||
<h3 class="font-bold text-slate-700 mb-3">⚡ Redis 配置</h3>
|
||||
<p class="text-xs text-slate-500 mb-3">请先在宿主机或 1Panel 安装 Redis。Docker 部署填主机 <code class="bg-slate-100 px-1 rounded">host.docker.internal</code>,端口 6379。</p>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 主机</label>
|
||||
<input x-model="form.redis_host" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
<input x-model="form.redis_host" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 端口</label>
|
||||
<input x-model="form.redis_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
||||
<input x-model="form.redis_db" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
<div class="mt-3" x-show="dockerDefaults">
|
||||
<p class="text-xs text-[var(--text-secondary)]">1Panel Redis <strong>无账号</strong>,只需填写下方密码(应用参数中的口令)。</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3" x-show="!dockerDefaults">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码(可选)</label>
|
||||
<input x-model="form.redis_pass" type="password" placeholder="无密码留空"
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名(可选)</label>
|
||||
<p class="text-xs text-[var(--text-secondary)] mb-1">自建 Redis ACL 可填用户名;多数环境留空</p>
|
||||
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="通常留空"
|
||||
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
||||
<input x-model="form.redis_db" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3" x-show="dockerDefaults">
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
||||
<input x-model="form.redis_db" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码</label>
|
||||
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel → Redis → 参数中的密码;无密码留空</p>
|
||||
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" placeholder="无密码留空"
|
||||
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -233,15 +249,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: credential check before init -->
|
||||
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
||||
<h3 class="font-bold text-slate-700 mb-2">🔗 MySQL / Redis 账号检测</h3>
|
||||
<p class="text-xs text-[var(--text-secondary)] mb-2">须先检测通过,才能初始化数据库(避免账号密码错误仍写入 .env)。</p>
|
||||
<div x-show="credLoading" class="text-sm text-[var(--text-secondary)] py-2">正在检测连接...</div>
|
||||
<div x-show="!credLoading && credChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-2">
|
||||
<template x-for="c in credChecks" :key="c.name">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0 text-sm">
|
||||
<span class="font-medium text-slate-700" x-text="c.name"></span>
|
||||
<span class="font-semibold text-right max-w-[70%]" :class="c.pass ? 'text-green-500' : 'text-red-500'" x-text="c.current"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div x-show="!credLoading && credAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg text-sm mb-2">
|
||||
<p>✓ MySQL 与 Redis 连接正常,可以初始化。</p>
|
||||
<p x-show="resolvedRedisUrl" class="mt-1 text-xs">Redis URL:<code class="bg-green-100 px-1 rounded" x-text="resolvedRedisUrl"></code></p>
|
||||
</div>
|
||||
<div x-show="!credLoading && !credAllPass && credChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg text-sm mb-2">
|
||||
连接未通过:请核对主机、用户名与密码后重新检测。
|
||||
</div>
|
||||
<button type="button" @click="testCredentials()" :disabled="credLoading || !form.db_pass"
|
||||
class="bg-slate-200 hover:bg-slate-300 disabled:opacity-50 text-slate-700 px-4 py-2 rounded-lg text-sm font-semibold transition">
|
||||
检测 MySQL / Redis 连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 border border-amber-200 text-amber-800 px-4 py-3 rounded-lg mt-4 text-sm">
|
||||
<b>⚠ 注意:</b>初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY
|
||||
③ 写入 .env + config.json + settings表 ④ 自动计算连接池大小
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-5">
|
||||
<button type="submit" :disabled="loading" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
||||
<button type="submit" :disabled="loading || !credAllPass" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
||||
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
|
||||
<span x-text="loading ? '初始化中...' : '初始化数据库 →'"></span>
|
||||
<span x-text="loading ? '初始化中...' : (credAllPass ? '初始化数据库 →' : '请先检测连接')"></span>
|
||||
</button>
|
||||
<button type="button" @click="step = 2" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
|
||||
</div>
|
||||
@@ -254,6 +296,31 @@
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-800 mb-4">👤 管理员账号 + 系统名称</h2>
|
||||
|
||||
<!-- Step 4: MySQL + Redis connection -->
|
||||
<div class="mb-4">
|
||||
<h3 class="font-bold text-slate-700 mb-2">🔗 MySQL / Redis 连接检测</h3>
|
||||
<div x-show="connLoading" class="text-sm text-[var(--text-secondary)] py-2">正在检测连接...</div>
|
||||
<div x-show="!connLoading && connChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-2">
|
||||
<template x-for="c in connChecks" :key="c.name">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0 text-sm">
|
||||
<span class="font-medium text-slate-700" x-text="c.name"></span>
|
||||
<span class="font-semibold" :class="c.pass ? 'text-green-500' : 'text-red-500'" x-text="c.current"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div x-show="!connLoading && connAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg text-sm mb-2">
|
||||
<p>✓ MySQL 与 Redis 均已连通,可创建管理员。</p>
|
||||
<p x-show="redisRepaired" class="mt-1 text-xs">已自动修正 .env 中的 Redis URL。</p>
|
||||
<p x-show="resolvedRedisUrl" class="mt-1 text-xs">Redis URL:<code class="bg-green-100 px-1 rounded" x-text="resolvedRedisUrl"></code></p>
|
||||
</div>
|
||||
<div x-show="!connLoading && !connAllPass && connChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg text-sm mb-2">
|
||||
连接未通过:若步骤 3 已初始化,请在服务器修正 /app/.env 中 NEXUS_DATABASE_URL / NEXUS_REDIS_URL 后重启容器,再点重新检测。
|
||||
</div>
|
||||
<button type="button" @click="checkConnections()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-4 py-2 rounded-lg text-sm font-semibold transition">
|
||||
重新检测连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 summary -->
|
||||
<div class="bg-slate-50 rounded-lg p-4 mb-4 text-sm">
|
||||
<div class="font-semibold text-green-600 mb-2">✓ 步骤 3 配置完成</div>
|
||||
@@ -301,9 +368,9 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-5">
|
||||
<button type="submit" :disabled="loading" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
||||
<button type="submit" :disabled="loading || !connAllPass" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
||||
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
|
||||
<span x-text="loading ? '创建中...' : '创建账号 →'"></span>
|
||||
<span x-text="loading ? '创建中...' : (connAllPass ? '创建账号 →' : '须先通过连接检测')"></span>
|
||||
</button>
|
||||
<button type="button" @click="step = 3" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
|
||||
</div>
|
||||
@@ -566,6 +633,15 @@ function installWizard() {
|
||||
envLoading: false,
|
||||
envChecks: [],
|
||||
envAllPass: false,
|
||||
connLoading: false,
|
||||
connChecks: [],
|
||||
connAllPass: false,
|
||||
redisRepaired: false,
|
||||
resolvedRedisUrl: '',
|
||||
dockerDefaults: null,
|
||||
credLoading: false,
|
||||
credChecks: [],
|
||||
credAllPass: false,
|
||||
initResult: null,
|
||||
installToken: '',
|
||||
installDir: '/opt/nexus',
|
||||
@@ -580,6 +656,7 @@ function installWizard() {
|
||||
redis_host: '127.0.0.1',
|
||||
redis_port: '6379',
|
||||
redis_db: '0',
|
||||
redis_user: '',
|
||||
redis_pass: '',
|
||||
api_port: '8600',
|
||||
timezone: 'Asia/Shanghai',
|
||||
@@ -600,6 +677,46 @@ function installWizard() {
|
||||
return results.every(r => !r.includes('✗') && !r.includes('⚠'));
|
||||
},
|
||||
|
||||
isStaleDockerHost(host) {
|
||||
const h = (host || '').trim().toLowerCase();
|
||||
return !h || h === 'localhost' || h === '127.0.0.1' || h === 'host.docker.internal';
|
||||
},
|
||||
|
||||
isStaleRedisUser(user) {
|
||||
const u = (user || '').trim().toLowerCase();
|
||||
return u === 'root';
|
||||
},
|
||||
|
||||
applyDockerDefaults(defaults) {
|
||||
if (!defaults) {
|
||||
this.dockerDefaults = null;
|
||||
return;
|
||||
}
|
||||
this.dockerDefaults = defaults;
|
||||
this.form.redis_user = '';
|
||||
if (this.isStaleDockerHost(this.form.db_host) && defaults.db_host) {
|
||||
this.form.db_host = defaults.db_host;
|
||||
}
|
||||
if (this.isStaleDockerHost(this.form.redis_host) && defaults.redis_host) {
|
||||
this.form.redis_host = defaults.redis_host;
|
||||
}
|
||||
if (!this.form.db_name || this.form.db_name === 'Nexus') this.form.db_name = defaults.db_name || this.form.db_name;
|
||||
if (!this.form.db_user || this.form.db_user === 'Nexus') this.form.db_user = defaults.db_user || this.form.db_user;
|
||||
if (defaults.db_port) this.form.db_port = defaults.db_port;
|
||||
if (defaults.redis_port) this.form.redis_port = defaults.redis_port;
|
||||
if (defaults.redis_db) this.form.redis_db = defaults.redis_db;
|
||||
if (this.isStaleRedisUser(this.form.redis_user)) this.form.redis_user = '';
|
||||
},
|
||||
|
||||
async refreshDockerDefaults() {
|
||||
try {
|
||||
const r = await fetch(API + '/api/install/env-check');
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
this.applyDockerDefaults(d.docker_defaults);
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Auto-detect site URL
|
||||
const proto = location.protocol === 'https:' ? 'https' : 'http';
|
||||
@@ -627,8 +744,76 @@ function installWizard() {
|
||||
});
|
||||
if (d.install_dir) this.installDir = d.install_dir;
|
||||
if (d.pool_size) this.initResult = { pool_size: d.pool_size, max_overflow: d.max_overflow, guardian_results: d.guardian_results || [] };
|
||||
if (d.step >= 4) this.checkConnections();
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// 1Panel: override stale host.docker.internal with container names from API/volume
|
||||
await this.refreshDockerDefaults();
|
||||
},
|
||||
|
||||
invalidateCredCheck() {
|
||||
this.credAllPass = false;
|
||||
this.credChecks = [];
|
||||
},
|
||||
|
||||
installFormPayload() {
|
||||
const payload = { ...this.form };
|
||||
if (this.dockerDefaults) payload.redis_user = '';
|
||||
return payload;
|
||||
},
|
||||
|
||||
async testCredentials() {
|
||||
this.credLoading = true;
|
||||
this.error = '';
|
||||
this.credAllPass = false;
|
||||
this.resolvedRedisUrl = '';
|
||||
try {
|
||||
const r = await fetch(API + '/api/install/test-credentials', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(this.installFormPayload()),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok) {
|
||||
this.credChecks = d.checks || [];
|
||||
this.credAllPass = !!d.all_pass;
|
||||
this.resolvedRedisUrl = d.resolved_redis_url || '';
|
||||
if (!this.credAllPass) {
|
||||
this.error = 'MySQL 或 Redis 连接未通过,请核对配置后重试。';
|
||||
}
|
||||
} else {
|
||||
this.credChecks = d.checks || [];
|
||||
this.error = d.detail || '连接检测失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = '连接检测请求失败: ' + e.message;
|
||||
}
|
||||
this.credLoading = false;
|
||||
},
|
||||
|
||||
async checkConnections() {
|
||||
this.connLoading = true;
|
||||
this.redisRepaired = false;
|
||||
this.resolvedRedisUrl = '';
|
||||
try {
|
||||
const r = await fetch(API + '/api/install/connection-check');
|
||||
if (r.ok) {
|
||||
const d = await r.json();
|
||||
this.connChecks = d.checks || [];
|
||||
this.connAllPass = !!d.all_pass;
|
||||
this.redisRepaired = !!d.redis_repaired;
|
||||
this.resolvedRedisUrl = d.resolved_redis_url || '';
|
||||
} else {
|
||||
const d = await r.json().catch(() => ({}));
|
||||
this.connChecks = [{ name: '连接检测', pass: false, current: d.detail || '请先完成步骤3' }];
|
||||
this.connAllPass = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.connChecks = [{ name: '连接检测', pass: false, current: '请求失败: ' + e.message }];
|
||||
this.connAllPass = false;
|
||||
}
|
||||
this.connLoading = false;
|
||||
},
|
||||
|
||||
async checkEnv() {
|
||||
@@ -641,7 +826,7 @@ function installWizard() {
|
||||
this.envChecks = d.checks;
|
||||
this.envAllPass = d.all_pass;
|
||||
if (d.docker_defaults) {
|
||||
Object.assign(this.form, d.docker_defaults);
|
||||
this.applyDockerDefaults(d.docker_defaults);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -658,7 +843,7 @@ function installWizard() {
|
||||
const r = await fetch(API + '/api/install/init-db', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(this.form),
|
||||
body: JSON.stringify(this.installFormPayload()),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok && d.success) {
|
||||
@@ -667,6 +852,7 @@ function installWizard() {
|
||||
this.installToken = d.install_token || '';
|
||||
this.btPanel = !!d.bt_panel;
|
||||
this.step = 4;
|
||||
await this.checkConnections();
|
||||
} else {
|
||||
this.error = d.detail || '初始化失败';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user