20 Commits

Author SHA1 Message Date
Nexus Deploy b11360015d fix(install): 1Panel Redis 无账号与步骤 4 URL 自愈
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
确立 Redis 仅密码认证(:pass@/default),隐藏误导性用户名字段;
connection-check 自动修正错误 NEXUS_REDIS_URL,并更新运维文档与验收脚本。
2026-06-06 07:51:52 +08:00
Nexus Deploy 3b2856f388 fix(install): auto-resolve 1Panel Redis auth (:pass@ / default)
Stop prefilling root; probe multiple Redis URL formats on credential
check and persist the URL that works for init-db.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 07:15:57 +08:00
Nexus Deploy 9d9fbc62f5 docs: add 1Panel operations knowledge SSOT and Redis test script
Consolidate install wizard, networking, URL formats, and troubleshooting
into docs/project/nexus-1panel-operations-knowledge.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 07:08:42 +08:00
Nexus Deploy 807f4c2448 fix(install): correct Redis URL auth for 1Panel root user
Password must use redis://:pass@ or redis://root:pass@ host; wizard adds
redis_user field defaulting to root on 1Panel Docker installs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:59:45 +08:00
Nexus Deploy da061d35db feat(install): require MySQL/Redis credential check at wizard step 3
Add test-credentials API and UI gate before init-db writes .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:47:22 +08:00
Nexus Deploy d0544c9bd2 fix(1panel): grant nexus@% for Docker MySQL 1045 on install
Add fix-1panel-mysql-grant.sh and clearer install wizard errors when
1Panel MySQL only allows nexus@localhost.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:19:19 +08:00
Nexus Deploy 3f5eacae60 feat(deploy): add 1Panel install wizard verify script
Server-side checklist for 1panel-network, container host sync, and
install API defaults before completing /app/install.html.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:05:15 +08:00
Nexus Deploy b25d0798f6 fix(1panel): sync MySQL/Redis container hosts on nx update
Upgrade was skipping 1panel-network detection; wizard now overrides stale host.docker.internal from volume/API defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:47:43 +08:00
Nexus Deploy 6b1d23c7d7 fix(1panel): sync Redis URL to container name on 1panel-network
Update NEXUS_REDIS_URL when detecting 1Panel-redis-*; add Redis TCP precheck and connection errors matching MySQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:39:52 +08:00
Nexus Deploy 6951b8049f fix(1panel): join 1panel-network and connect MySQL/Redis by container name
Per 1Panel App Store architecture, bridge apps must use Docker DNS on 1panel-network instead of host.docker.internal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:35:04 +08:00
Nexus Deploy da0424a268 fix(install): MySQL TCP probe and clearer host.docker.internal errors
Step 2 now detects whether host MySQL is listening; init-db fails fast with 1Panel setup guidance on error 2003.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:22:16 +08:00
Nexus Deploy 569263dddb fix(install): remove redundant Redis/MySQL host hints in wizard step 3
Docker defaults already prefill host.docker.internal; drop duplicate 1Panel install reminders from the form.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:08:55 +08:00
Nexus Deploy 646929ddff feat(deploy): register nx globally via install-nx-cli.sh
Centralize chmod and /usr/local/bin symlinks; install, upgrade, update, and every nx invocation refresh the global nx command.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:43:00 +08:00
Nexus Deploy 0a7c5ee617 fix(deploy): set executable bit on nx and refresh symlinks on update
Git tracked deploy/nx as 644 so /usr/local/bin/nx failed with Permission denied after pull; update.sh now chmods CLI scripts before upgrade.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:39:33 +08:00
Nexus Deploy 93400b7793 fix(install): split Redis install check (step 2) from connection test (step 4)
Step 2 only TCP-probes host Redis without blocking the wizard; step 4 runs /connection-check on MySQL and Redis from .env before creating the admin account.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:32:47 +08:00
Nexus Deploy a5aa5f81ad docs(deploy): merge nx god into unified menu and expand README
Consolidate install and ops into a single nx menu with one-click update via update.sh; document all curl install/update entry points in the root README.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:27:11 +08:00
Nexus Deploy 86c865e710 feat(deploy): add update.sh and always rebuild on upgrade
Upgrade no longer exits when Git is already up to date; it still rebuilds the Nexus image to pick up entrypoint changes. Add deploy/update.sh as the one-command entry point.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:16:15 +08:00
Nexus Deploy 1bd69077a2 fix(deploy): auto-remove legacy Compose MySQL containers on upgrade
Run uninstall-mysql-compose before install/upgrade and pass --remove-orphans so nexus-prod mysql orphans are stopped after the service is dropped from compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 03:32:00 +08:00
Nexus Deploy f37ebf8621 feat(docker): remove bundled MySQL from production Compose stack
Production installs now run only the Nexus container; MySQL and Redis are self-hosted on the machine or 1Panel, with install wizard defaults pointing at host.docker.internal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 03:27:57 +08:00
Nexus Deploy e42dc77127 feat(deploy): add redis compose uninstall; require Redis in install wizard
Step 2 blocks until Redis connects; uninstall-redis-compose.sh removes legacy
nexus-prod-redis container without touching host/1Panel Redis.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:51:52 +08:00
50 changed files with 3114 additions and 472 deletions
+1
View File
@@ -69,6 +69,7 @@ web/uploads/
# Data (sensitive)
web/data/config.php
web/data/*.db
web/data/1panel-hosts.json
# Backups
backups/
+71 -94
View File
@@ -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
View File
@@ -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` | 仅装 Nexus1Panel 已就绪) | `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 Docker1Panel 已就绪)
## 仅 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 Docker1Panel 已装好 |
| `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 / Redis1Panel 应用商店)
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`
### 常见错误 1045Access 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
```
+43
View File
@@ -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}"
+70
View File
@@ -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
View File
Regular → Executable
+6 -18
View File
@@ -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 Dockercurl 入口)"
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 ""
}
+68
View File
@@ -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
View File
@@ -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_hostNEXUS_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-networkMySQL/Redis 使用 1Panel 容器名互联)"
fi
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
info "构建并启动(首次约 1020 分钟)..."
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 ;;
*)
Regular → Executable
+147 -134
View File
@@ -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核16G4c16g
@@ -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 "重启 NexusPython..."
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] 重启 NexusPython 应用)
[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] 重启 NexusPython
[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
View File
View File
+91
View File
@@ -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 "安装向导步骤 31Panel 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 "$@"
+116
View File
@@ -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"
+116
View File
@@ -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"
+15
View File
@@ -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
View File
+306
View File
@@ -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 "已包含 b25d0791Panel 容器名 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. 步骤 2MySQL/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 "$@"
+8 -8
View File
@@ -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
# 1Panelnx 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
View File
@@ -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` 仅查更新)
+15
View File
@@ -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
+5 -33
View File
@@ -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:
+6 -5
View File
@@ -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 -4
View File
@@ -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
-3
View File
@@ -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
+2 -5
View File
@@ -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
View File
@@ -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` 步骤 3MySQL/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
```
步骤 3Redis 用户 `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` | 本地 devnexus + 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` |
**历史 bug807f4c2 前)**:生成 `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` 自动 syncb25d079+
`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` 为准。*
+18 -13
View File
@@ -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
View File
@@ -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")
+149
View File
@@ -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
View File
@@ -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 || '初始化失败';
}