Compare commits
20 Commits
bfda70bbe7
...
536d520744
| Author | SHA1 | Date | |
|---|---|---|---|
| 536d520744 | |||
| 6276ea08b7 | |||
| c81fe3183f | |||
| 5e7c5beb43 | |||
| 9b7dba99ff | |||
| 222703d733 | |||
| d884ea00ca | |||
| 422dcae0ff | |||
| 343def7e77 | |||
| 83ce11f55c | |||
| e613aecc8e | |||
| f71da47ccf | |||
| 1acac6439a | |||
| feda614c52 | |||
| 6cef3d2942 | |||
| 00f44032ff | |||
| 8ddcf2ad0d | |||
| 65e4478b4b | |||
| 2f830424a4 | |||
| 7b33b745a0 |
@@ -11,6 +11,8 @@ build/
|
||||
.install_locked
|
||||
.install_state.json
|
||||
SECRETS.md
|
||||
.git-askpass-nexus.sh
|
||||
deploy/nexus-1panel.secrets.sh
|
||||
*.pem
|
||||
.env.*
|
||||
.venv/
|
||||
|
||||
+14
-1
@@ -27,8 +27,21 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY server/ ./server/
|
||||
COPY web/agent/ ./web/agent/
|
||||
COPY --from=frontend /build/web/app ./web/app/
|
||||
# Install wizard (Alpine + Tailwind static page; not part of Vite build)
|
||||
COPY web/app/install.html ./web/app/install.html
|
||||
RUN mkdir -p ./web/app/vendor
|
||||
COPY web/app/vendor/tailwindcss-browser.js \
|
||||
web/app/vendor/theme-init.js \
|
||||
web/app/vendor/theme.css \
|
||||
web/app/vendor/alpinejs.min.js \
|
||||
./web/app/vendor/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh \
|
||||
&& test -f /app/web/app/install.html \
|
||||
&& test -f /app/web/app/vendor/alpinejs.min.js \
|
||||
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
||||
&& test -f /app/web/app/vendor/theme.css \
|
||||
&& test -f /app/web/app/vendor/theme-init.js
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# Nexus 6.0
|
||||
|
||||
面向 **2000+ 服务器** 的运维管理平台:FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram(后端),Vue 3 + Vuetify 4 + TypeScript SPA(前端 14 页)。
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 生产域名 | https://api.synaglobal.vip |
|
||||
| 默认安装目录 | `/opt/nexus` |
|
||||
| 后端端口 | `8600`(1Panel 反代到 `http://127.0.0.1:8600`) |
|
||||
| 安装向导 | `https://你的域名/app/install.html` |
|
||||
| 文档索引 | [docs/README.md](docs/README.md) |
|
||||
|
||||
---
|
||||
|
||||
## 一键安装(公共仓库,推荐)
|
||||
|
||||
在 **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
|
||||
```
|
||||
|
||||
指定资源档位(`1c4g` / `2c8g` 默认 / `4c16g`):
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
可选:先下载再执行(便于审计脚本内容):
|
||||
|
||||
```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`)。
|
||||
|
||||
---
|
||||
|
||||
## 已 clone 到本机(跳过再次拉仓)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
```
|
||||
|
||||
或交互菜单:
|
||||
|
||||
```bash
|
||||
nx
|
||||
# 选「全新安装」或子命令: nx install-fresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NX 运维菜单(装好后)
|
||||
|
||||
安装脚本会向 `PATH` 注册:
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `nx` | 交互主菜单(安装 / 升级 / 健康检查) |
|
||||
| `nx god` | **上帝菜单**:重启 Python、全栈重启、日志、升级、备份等 |
|
||||
| `nexus-fresh` | 等同 `install-nexus-fresh.sh` |
|
||||
| `nexus-install` | 快捷安装入口 |
|
||||
|
||||
常用子命令:
|
||||
|
||||
```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`
|
||||
|
||||
---
|
||||
|
||||
## 部署脚本一览(`deploy/`)
|
||||
|
||||
### 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) |
|
||||
|
||||
详细说明:[deploy/README-1panel.md](deploy/README-1panel.md)
|
||||
|
||||
### Docker Compose 与档位
|
||||
|
||||
| 路径 | 用途 |
|
||||
|------|------|
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(变量化 CPU/内存) |
|
||||
| [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 部署(宝塔 / 裸机)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [install.sh](deploy/install.sh) | 传统安装 |
|
||||
| [upgrade.sh](deploy/upgrade.sh) | 拉代码 + 重启 |
|
||||
| [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` |
|
||||
| [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) | 快速验证入口 |
|
||||
|
||||
---
|
||||
|
||||
## 迁机 vs 全新安装
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| **新 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` 重启即可 |
|
||||
|
||||
---
|
||||
|
||||
## 技术栈与目录
|
||||
|
||||
```
|
||||
Nexus/
|
||||
├── server/ # FastAPI 后端(Clean Architecture)
|
||||
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
|
||||
├── web/app/ # 静态资源挂载点(构建产物 gitignore)
|
||||
├── deploy/ # 安装、门控、健康检查、运维脚本
|
||||
├── 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)
|
||||
|
||||
---
|
||||
|
||||
## 文档与规范
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [docs/README.md](docs/README.md) | 文档总索引(SSOT) |
|
||||
| [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/`
|
||||
|
||||
---
|
||||
|
||||
## 许可与仓库
|
||||
|
||||
| 用途 | 地址 |
|
||||
|------|------|
|
||||
| 仓库首页(产品说明 / README) | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git 克隆 / push | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
|
||||
- 分支:`main`
|
||||
- 提交前请通过 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
|
||||
**注意**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md` 含敏感信息,**勿提交到 Git**。生产密钥仅保存在服务器与本机私密文档中。
|
||||
@@ -0,0 +1,101 @@
|
||||
# 1Panel + Docker 一键部署
|
||||
|
||||
## 重装服务器(推荐: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
|
||||
```
|
||||
|
||||
已装 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
|
||||
```
|
||||
|
||||
## 仅 Nexus Docker(1Panel 已就绪)
|
||||
|
||||
```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` 完成向导。
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## NX 运维菜单
|
||||
|
||||
```bash
|
||||
nx # 主菜单
|
||||
nx god # 上帝菜单(重启 Python / 升级 / 日志)
|
||||
```
|
||||
|
||||
装好后会注册:`nexus-install` · `nx` · `nexus-fresh`
|
||||
|
||||
## 脚本说明
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `install-1panel-docker.sh` | **重装服务器**一键链(1Panel + Docker + Nexus) |
|
||||
| `quick-install.sh` | 仅 Nexus Docker(1Panel 已装好) |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装(被上面两个脚本调用) |
|
||||
| `nexus-1panel.sh` | 迁机 / 升级底层 |
|
||||
| `nx` | 交互菜单 |
|
||||
|
||||
## 资源档位
|
||||
|
||||
`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
|
||||
## 密钥(每台新服务器唯一)
|
||||
|
||||
全新安装(`install-nexus-fresh.sh` / `--fresh`)会调用 `scripts/generate_nexus_secrets.py` 自动生成:
|
||||
|
||||
- `MYSQL_ROOT_PASSWORD` / `MYSQL_PASSWORD`
|
||||
- `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(与安装向导算法一致)
|
||||
|
||||
写入 `docker/.env.prod`,备份到 `/root/.nexus-install-secrets-*.env`(chmod 600)。
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 新机(推荐) | `curl \| bash` 或 `install-nexus-fresh.sh` |
|
||||
| 迁机保留旧密钥 | `nexus-1panel.sh install --from-env /path/to/old/.env` |
|
||||
| 显式复用 secrets 文件 | `nexus-1panel.sh install --reuse-secrets`(**勿用于新机**) |
|
||||
|
||||
**Gitea 令牌**:公共仓库 `curl | bash` / `git clone` **不需要**配置。仅私人仓库时可选 `/root/.nexus-deploy.env` 或 `nexus-1panel.sh init-token`。
|
||||
|
||||
## 安装向导 `/app/install.html`
|
||||
|
||||
生产镜像 `Dockerfile.prod` 已内置 `install.html` + `vendor`(Alpine/Tailwind)。升级后若 404:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/nexus-1panel.sh upgrade # 会 --build 重建镜像
|
||||
# 或仅同步到当前容器(旧镜像兜底):
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run ON production server (宝塔终端 / ssh root@47.254.123.106)
|
||||
# Pulls latest main from Gitea and restarts Nexus. Frontend tar must be uploaded separately
|
||||
# if you did not build on this machine.
|
||||
#
|
||||
# Usage (on server):
|
||||
# cd /www/wwwroot/api.synaglobal.vip
|
||||
# bash deploy/deploy-on-server.sh
|
||||
#
|
||||
# Optional env:
|
||||
# NEXUS_GITEA_TOKEN — if git pull needs auth
|
||||
# SKIP_FRONTEND=1 — backend only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/www/wwwroot/api.synaglobal.vip}"
|
||||
cd "${DEPLOY_PATH}"
|
||||
|
||||
echo "═══ Nexus server-side deploy ═══"
|
||||
echo "Path: ${DEPLOY_PATH}"
|
||||
|
||||
if [ -n "${NEXUS_GITEA_TOKEN:-}" ]; then
|
||||
git remote set-url origin "http://admin:${NEXUS_GITEA_TOKEN}@66.154.115.8:3000/admin/Nexus.git"
|
||||
fi
|
||||
|
||||
echo "▶ git fetch + reset --hard origin/main..."
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
echo " HEAD: $(git log -1 --oneline)"
|
||||
|
||||
echo "▶ supervisorctl restart nexus..."
|
||||
supervisorctl restart nexus
|
||||
|
||||
if [ -f /tmp/nexus-frontend.tar.gz ] && [ "${SKIP_FRONTEND:-0}" != "1" ]; then
|
||||
echo "▶ extract /tmp/nexus-frontend.tar.gz..."
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app
|
||||
rm -f /tmp/nexus-frontend.tar.gz
|
||||
if [ -f deploy/prune_frontend_assets.py ]; then
|
||||
python3 deploy/prune_frontend_assets.py --dry-run
|
||||
python3 deploy/prune_frontend_assets.py
|
||||
fi
|
||||
supervisorctl restart nexus
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(curl -s http://127.0.0.1:8600/health || true)
|
||||
SPA=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/ || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
|
||||
if [ "${HEALTH}" = "ok" ] && [ "${SPA}" = "200" ]; then
|
||||
echo "✓ Server deploy OK"
|
||||
else
|
||||
echo "✗ Verify failed — check: supervisorctl status nexus" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# 下载并执行 install-nexus-fresh.sh(公共仓库无需令牌)
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
|
||||
#
|
||||
# 推荐直接使用:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
OUT="${OUT:-/tmp/install-nexus-fresh.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
|
||||
download() {
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/install-nexus-fresh.sh"
|
||||
if curl -fsSL "$url" -o "$OUT" 2>/dev/null && head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载 install-nexus-fresh.sh"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
done
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
|
||||
if curl -fsSL -H "Authorization: token ${NEXUS_GITEA_TOKEN}" "$url" -o "$OUT" 2>/dev/null \
|
||||
&& head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载(API + 令牌)"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! download "$@"; then
|
||||
err "下载失败,请改用公共仓库一键入口:"
|
||||
err " curl -fsSL \"${NEXUS_RAW_BASE}/deploy/quick-install.sh\" | bash"
|
||||
exit 1
|
||||
fi
|
||||
@@ -156,3 +156,13 @@
|
||||
{"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"changelog","result":"PASS","detail":"2026-06-04-audit-phase2-p2-6-sync-engine-full.md 32lines"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"audit","result":"PASS","detail":"2026-06-04-docs-consolidation.md"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 重装服务器一键入口(1Panel + Docker + Nexus)
|
||||
#
|
||||
# 适用:全新 VPS / 重装系统后,root 一条命令装完整栈。
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
#
|
||||
# 4 核 16G + 自定义域名:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g --domain api.example.com
|
||||
#
|
||||
# 已装 1Panel,只装 Nexus Docker:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
#
|
||||
# 流程:
|
||||
# ① 可选:官方 1Panel 安装(含 Docker,交互式)
|
||||
# ② 检测 / 安装 Docker Compose
|
||||
# ③ install-nexus-fresh.sh → MySQL + Nexus 容器(Redis 自行安装)
|
||||
#
|
||||
# 反代:必须在 1Panel 面板 UI 配置,禁止脚本直接改 /opt/1panel/apps/
|
||||
# 示例: deploy/1panel/openresty-nexus.conf.example
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
|
||||
PANEL_INSTALL_URL="${PANEL_INSTALL_URL:-https://resource.1panel.pro/v2/quick_start.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
WITH_1PANEL=auto
|
||||
SKIP_1PANEL=false
|
||||
SKIP_DOCKER_INSTALL=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
重装服务器 — 1Panel + Docker + Nexus 一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/install-1panel-docker.sh" | bash
|
||||
|
||||
选项(传给 bash -s --):
|
||||
--with-1panel 未装 1Panel 时先跑官方安装脚本(交互式)
|
||||
--skip-1panel 跳过 1Panel,只装 Nexus Docker 栈
|
||||
--skip-docker-install 不自动装 Docker(已在 1Panel 装好)
|
||||
--profile 2c8g|4c16g|1c4g
|
||||
--domain NAME
|
||||
--root PATH
|
||||
-h, --help
|
||||
|
||||
说明:
|
||||
- 1Panel 官方安装为交互式(端口/用户/密码),装完记下面板地址
|
||||
- Nexus 密钥按本机自动生成,备份在 /root/.nexus-install-secrets-*.env
|
||||
- 网站反代请在 1Panel UI 操作,勿手工改 /opt/1panel/apps/
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--with-1panel) WITH_1PANEL=true; shift ;;
|
||||
--skip-1panel) SKIP_1PANEL=true; WITH_1PANEL=false; shift ;;
|
||||
--skip-docker-install) SKIP_DOCKER_INSTALL=true; shift ;;
|
||||
--profile|--domain|--root)
|
||||
EXTRA_ARGS+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--skip-clone|--skip-1panel-check)
|
||||
EXTRA_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus — 1Panel + Docker + Nexus 一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
load_deploy_env() {
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
}
|
||||
|
||||
has_1panel() {
|
||||
command -v 1pctl >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_docker_compose() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
if [[ "$SKIP_1PANEL" == true ]]; then
|
||||
info "跳过 1Panel 安装(--skip-1panel)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_1panel; then
|
||||
info "已安装 1Panel"
|
||||
if 1pctl user-info >/dev/null 2>&1; then
|
||||
1pctl user-info 2>/dev/null | grep -E '面板地址|面板用户' || true
|
||||
else
|
||||
warn "1pctl user-info 失败,面板可能需修复;继续安装 Nexus…"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" == auto ]]; then
|
||||
echo ""
|
||||
warn "未检测到 1Panel(1pctl 不存在)"
|
||||
echo " ① 先装 1Panel(推荐,含 Docker)再装 Nexus"
|
||||
echo " ② 跳过 1Panel,仅装 Nexus Docker(需本机已有 Docker)"
|
||||
echo ""
|
||||
read -r -p "是否现在安装 1Panel? [Y/n]: " ans
|
||||
case "${ans:-Y}" in
|
||||
n|N) WITH_1PANEL=false ;;
|
||||
*) WITH_1PANEL=true ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" != true ]]; then
|
||||
warn "未安装 1Panel,将仅安装 Nexus Docker 栈"
|
||||
return 0
|
||||
fi
|
||||
|
||||
step "安装 1Panel(官方脚本,交互式)..."
|
||||
info "下载: $PANEL_INSTALL_URL"
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/1panel-quick-start.XXXXXX.sh)"
|
||||
if ! curl -fsSL "$PANEL_INSTALL_URL" -o "$tmp"; then
|
||||
error "下载 1Panel 安装脚本失败"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$tmp"
|
||||
echo ""
|
||||
warn "接下来为 1Panel 官方安装向导,请按提示设置端口、安全入口、用户名、密码"
|
||||
warn "安装目录建议默认 /opt"
|
||||
echo ""
|
||||
bash "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
if ! has_1panel; then
|
||||
error "1Panel 安装后仍未找到 1pctl,请检查安装日志"
|
||||
exit 1
|
||||
fi
|
||||
info "1Panel 安装完成"
|
||||
1pctl user-info 2>/dev/null || true
|
||||
echo ""
|
||||
warn "请在 1Panel → 应用商店 安装 OpenResty(用于 HTTPS 反代)"
|
||||
read -r -p "OpenResty 已装好或稍后自行安装,按 Enter 继续 Nexus 安装..."
|
||||
}
|
||||
|
||||
install_docker_engine() {
|
||||
step "安装 Docker Engine + Compose 插件..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl >/dev/null
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q docker docker-compose-plugin >/dev/null 2>&1 || \
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
else
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
}
|
||||
|
||||
phase_docker() {
|
||||
step "检测 Docker Compose..."
|
||||
if has_docker_compose; then
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_DOCKER_INSTALL" == true ]]; then
|
||||
error "未检测到 docker compose,且已指定 --skip-docker-install"
|
||||
error "请在 1Panel → 容器 安装 Docker,或去掉该参数"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
warn "未检测到 docker compose"
|
||||
if has_1panel; then
|
||||
warn "建议在 1Panel → 容器 安装 Docker 后重新运行本脚本"
|
||||
read -r -p "是否尝试用 get.docker.com 自动安装? [y/N]: " ans
|
||||
[[ "${ans:-N}" == y || "${ans:-N}" == Y ]] || exit 1
|
||||
else
|
||||
info "自动安装 Docker(get.docker.com)..."
|
||||
fi
|
||||
|
||||
install_docker_engine
|
||||
|
||||
if ! has_docker_compose; then
|
||||
error "Docker 安装后仍无 compose 插件,请手动安装 docker-compose-plugin"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose 就绪"
|
||||
}
|
||||
|
||||
download_fresh_installer() {
|
||||
local name="install-nexus-fresh.sh" dest
|
||||
dest="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && head -1 "$dest" | grep -q '^#!/'; then
|
||||
chmod +x "$dest"
|
||||
echo "$dest"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
phase_nexus() {
|
||||
step "安装 Nexus Docker 栈(MySQL + Nexus;Redis 请自行安装)..."
|
||||
local installer
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" ]]; then
|
||||
installer="${NEXUS_ROOT}/deploy/install-nexus-fresh.sh"
|
||||
info "使用本地: $installer"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
if installer="$(download_fresh_installer)"; then
|
||||
info "已下载 install-nexus-fresh.sh"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
error "无法获取 install-nexus-fresh.sh,请检查 Gitea 可达性"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
require_root
|
||||
load_deploy_env
|
||||
phase_1panel
|
||||
phase_docker
|
||||
phase_nexus
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 全新安装脚本(1Panel + Docker Compose)
|
||||
#
|
||||
# 适用:新机已装 1Panel / Docker,空库,走 /app/install.html 安装向导
|
||||
# 风格参考 1Panel quick_start.sh:分阶段检查 + 结束输出访问地址
|
||||
#
|
||||
# 用法(公共仓库,root 下一条命令,参考 1Panel):
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
#
|
||||
# bash deploy/install-nexus-fresh.sh --profile 4c16g
|
||||
#
|
||||
# 公共仓库匿名 clone,无需 Gitea 令牌。NEXUS 密钥由安装脚本按机自动生成。
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_install_self="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if ! head -1 "$_install_self" | grep -q '^#!/'; then
|
||||
echo "[ERROR] 脚本文件无效。若用 curl 下载,请改用: bash deploy/download-install-fresh.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$_install_self" 2>/dev/null; then
|
||||
echo "[ERROR] 下载到的是 Gitea 404 页面,不是安装脚本。" >&2
|
||||
echo " 请: cd /opt/nexus && git pull && sudo bash deploy/install-nexus-fresh.sh --skip-clone" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 默认配置(与 SECRETS.md / nexus-1panel 一致)──
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://66.154.115.8:3000/admin/Nexus/raw/branch/main}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 6.0 全新安装(1Panel + Docker)
|
||||
|
||||
sudo bash install-nexus-fresh.sh [选项]
|
||||
|
||||
选项:
|
||||
--domain NAME 公网域名(默认: ${NEXUS_DOMAIN})
|
||||
--profile NAME 资源档位 1c4g|2c8g|4c16g(默认: 2c8g)
|
||||
--root PATH 安装目录(默认: /opt/nexus)
|
||||
--skip-clone 已在 /opt/nexus,不重新 clone
|
||||
--skip-1panel-check 不检测 1Panel 是否已安装
|
||||
-h, --help 显示帮助
|
||||
|
||||
安装后请用浏览器打开: https://域名/app/install.html
|
||||
root 下全局命令: nexus-fresh | nx | nx god
|
||||
EOF
|
||||
}
|
||||
|
||||
SKIP_1PANEL_CHECK=false
|
||||
SKIP_CLONE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--skip-clone) SKIP_CLONE=true; shift ;;
|
||||
--skip-1panel-check) SKIP_1PANEL_CHECK=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 全新安装(1Panel + Docker Compose) ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 档位: ${NEXUS_PROFILE} 目录: ${NEXUS_ROOT} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
load_credentials() {
|
||||
step "加载凭据..."
|
||||
local f
|
||||
for f in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
if [[ -f "$f" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "$f"
|
||||
set +a
|
||||
info "已加载 $f"
|
||||
fi
|
||||
done
|
||||
if [[ -f "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
set +a
|
||||
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh(可选)"
|
||||
fi
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
info "全新安装:NEXUS 密钥将在 docker/.env.prod 中按本机自动生成"
|
||||
}
|
||||
|
||||
install_root_commands() {
|
||||
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
|
||||
chmod +x "${NEXUS_ROOT}/deploy/nx" \
|
||||
"${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" \
|
||||
"${NEXUS_ROOT}/deploy/nexus-1panel.sh" \
|
||||
"${NEXUS_ROOT}/deploy/sync-install-wizard-to-container.sh" 2>/dev/null || true
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nx" /usr/local/bin/nx
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nexus-1panel.sh" /usr/local/bin/nexus-1panel
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" /usr/local/bin/nexus-fresh
|
||||
ln -sf "${NEXUS_ROOT}/deploy/quick-install.sh" /usr/local/bin/nexus-install
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-1panel-docker.sh" /usr/local/bin/nexus-1panel-docker 2>/dev/null || true
|
||||
info "已注册全局命令(root 直接输入即可):"
|
||||
info " nexus-1panel-docker — 重装服务器(1Panel + Docker + Nexus)"
|
||||
info " nexus-install — 仅 Nexus Docker(curl 入口)"
|
||||
info " nx — 主菜单 / 上帝菜单"
|
||||
info " nexus-fresh — 全新安装脚本"
|
||||
}
|
||||
|
||||
phase_preflight() {
|
||||
step "阶段 1/6 — 环境检查"
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请切换到 root 再执行(不要用 sudo 包一层):"
|
||||
error " su -"
|
||||
error " bash $0 ..."
|
||||
exit 1
|
||||
fi
|
||||
info "运行用户: root(无需 sudo)"
|
||||
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
error "无法识别操作系统"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
info "系统: ${NAME:-Linux} ${VERSION_ID:-}"
|
||||
case "${ID:-}" in
|
||||
ubuntu|debian) info "受支持的分支: Debian/Ubuntu ✓" ;;
|
||||
centos|rhel|rocky|almalinux) info "受支持的分支: RHEL 系 ✓" ;;
|
||||
*) warn "未在官方列表中测试,可能仍可运行" ;;
|
||||
esac
|
||||
|
||||
local mem_kb
|
||||
mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||
if [[ "$mem_kb" -lt 3500000 ]]; then
|
||||
warn "内存 < 4GB,建议档位 1c4g: --profile 1c4g"
|
||||
else
|
||||
info "内存: $((mem_kb / 1024 / 1024)) GiB 量级"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
step "阶段 2/6 — 1Panel / Docker"
|
||||
if [[ "$SKIP_1PANEL_CHECK" != true ]]; then
|
||||
if command -v 1pctl >/dev/null 2>&1; then
|
||||
info "检测到 1Panel (1pctl)"
|
||||
if 1pctl version >/dev/null 2>&1; then
|
||||
info "1Panel: $(1pctl version 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
else
|
||||
warn "未检测到 1pctl — 若尚未安装 1Panel,可先执行:"
|
||||
warn " curl -sSL https://resource.1panel.pro/v2/quick_start.sh -o quick_start.sh"
|
||||
warn " bash quick_start.sh"
|
||||
read -r -p "继续安装 Nexus? [y/N]: " cont
|
||||
[[ "$cont" == "y" || "$cont" == "Y" ]] || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
error "未检测到 docker compose"
|
||||
error "请在 1Panel → 容器 中安装 Docker,或: apt install -y docker.io docker-compose-v2"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
}
|
||||
|
||||
phase_packages() {
|
||||
step "阶段 3/6 — 依赖包"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl openssl ca-certificates iproute2 >/dev/null 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q git curl openssl ca-certificates iproute >/dev/null 2>&1 || true
|
||||
fi
|
||||
info "git / curl 就绪"
|
||||
}
|
||||
|
||||
git_clone_url() {
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_clone() {
|
||||
step "阶段 4/6 — 获取 Nexus 代码"
|
||||
if [[ "$SKIP_CLONE" == true ]]; then
|
||||
if [[ ! -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
error "--skip-clone 但 ${NEXUS_ROOT} 不是 git 仓库"
|
||||
exit 1
|
||||
fi
|
||||
load_credentials
|
||||
info "跳过 clone,使用现有 ${NEXUS_ROOT}"
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
load_credentials
|
||||
local url
|
||||
url="$(git_clone_url)"
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
info "公共仓库:匿名 git clone(无需令牌)"
|
||||
fi
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
info "仓库已存在,同步 ${NEXUS_GIT_BRANCH}..."
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}"
|
||||
else
|
||||
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
|
||||
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
info "克隆 → $NEXUS_ROOT"
|
||||
git clone "$url" "$NEXUS_ROOT"
|
||||
git -C "$NEXUS_ROOT" checkout "$NEXUS_GIT_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
load_credentials
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
}
|
||||
|
||||
phase_install_stack() {
|
||||
step "阶段 5/6 — Docker 构建与启动(全新库,约 10~20 分钟)"
|
||||
export NEXUS_ROOT NEXUS_DOMAIN NEXUS_PROFILE
|
||||
bash "${NEXUS_ROOT}/deploy/nexus-1panel.sh" install \
|
||||
--fresh \
|
||||
--skip-clone \
|
||||
--profile "$NEXUS_PROFILE" \
|
||||
--domain "$NEXUS_DOMAIN"
|
||||
}
|
||||
|
||||
phase_post() {
|
||||
step "阶段 6/6 — 收尾"
|
||||
install_root_commands
|
||||
|
||||
local port health
|
||||
port="8600"
|
||||
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "pending")"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}Nexus 全新安装完成${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}"
|
||||
echo -e "${GREEN}║${NC} 本地健康: http://127.0.0.1:${port}/health → ${health} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}① 浏览器完成安装向导(必做)${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} https://${NEXUS_DOMAIN}/app/install.html ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}② 1Panel 创建网站并反代${NC} ${GREEN}║${NC}"
|
||||
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}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
if [[ "$SKIP_CLONE" == true && -d "${NEXUS_ROOT}/deploy" ]]; then
|
||||
install_root_commands
|
||||
fi
|
||||
phase_preflight
|
||||
phase_1panel
|
||||
phase_packages
|
||||
phase_clone
|
||||
phase_install_stack
|
||||
phase_post
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,14 @@
|
||||
# 可选 — 仅私人 Gitea 仓库需要(公共仓库匿名 clone,新机无需此文件)
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
|
||||
# NEXUS_GITEA_TOKEN='你的Gitea_Access_Token'
|
||||
|
||||
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
|
||||
# NEXUS_SECRET_KEY=''
|
||||
# NEXUS_API_KEY=''
|
||||
# NEXUS_ENCRYPTION_KEY=''
|
||||
|
||||
NEXUS_DOMAIN='api.synaglobal.vip'
|
||||
NEXUS_ROOT='/opt/nexus'
|
||||
NEXUS_PROFILE='2c8g'
|
||||
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
|
||||
@@ -0,0 +1,844 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — 1Panel + Docker 一键安装 / 升级(参数已全部内置,无需 export)
|
||||
#
|
||||
# 新机 1Panel 终端一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
|
||||
#
|
||||
# 其他:
|
||||
# bash deploy/nexus-1panel.sh install --profile 4c16g
|
||||
# bash deploy/nexus-1panel.sh upgrade
|
||||
# bash deploy/nexus-1panel.sh check
|
||||
# bash deploy/nexus-1panel.sh ports
|
||||
#
|
||||
# 公共仓库匿名 clone,无需 Gitea 令牌。
|
||||
# 仅私人仓库可选: deploy/nexus-1panel.secrets.sh 或 /root/.nexus-deploy.env
|
||||
# 或 clone 时 URL 带令牌,脚本会从 git remote 自动解析
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# 内置路径/域名(无敏感信息,可提交 Git)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
NEXUS_FROM_ENV="${NEXUS_FROM_ENV:-/www/wwwroot/api.synaglobal.vip/.env}"
|
||||
NEXUS_BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||||
NEXUS_DEPLOY_ENV="${NEXUS_DEPLOY_ENV:-/root/.nexus-deploy.env}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
|
||||
COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
ENV_PROD="docker/.env.prod"
|
||||
HOST_PROFILE_FILE="docker/.host-profile"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
SKIP_CLONE=false
|
||||
FRESH_INSTALL=false
|
||||
REUSE_SECRETS=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
NO_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
CHECK_ONLY=false
|
||||
DRY_RUN=false
|
||||
CUSTOM_CPUS=""
|
||||
CUSTOM_MEM_GB=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Nexus 1Panel 一键脚本
|
||||
|
||||
bash deploy/nexus-1panel.sh install [选项] 克隆/配置/启动
|
||||
bash deploy/nexus-1panel.sh upgrade [选项] 拉代码+备份+重建
|
||||
bash deploy/nexus-1panel.sh check 检查远程是否有更新
|
||||
bash deploy/nexus-1panel.sh ports 检查 Gitea/8600/80/443 与防火墙
|
||||
bash deploy/nexus-1panel.sh init-token <令牌> 覆盖内置 Gitea 令牌(一般不需要)
|
||||
|
||||
资源档位(三选一):
|
||||
--profile 2c8g|4c16g|1c4g 预设(默认 2c8g = 2核8G)
|
||||
--cpus N --mem-gb N 自动匹配最近预设
|
||||
|
||||
安装选项:
|
||||
--skip-clone 已在 /opt/nexus,仅 pull
|
||||
--fresh 全新安装(本机自动生成唯一密钥 + /app/install.html)
|
||||
--from-env PATH 从旧 .env 迁机导入密钥
|
||||
--reuse-secrets 使用 secrets 文件中的 NEXUS 密钥(勿用于新机)
|
||||
--domain NAME 覆盖域名
|
||||
|
||||
升级选项:
|
||||
--no-backup --prune --dry-run
|
||||
|
||||
Git: 公共仓库匿名 clone(无需令牌);私人仓库见 init-token
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
script_dir() {
|
||||
dirname "$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
}
|
||||
|
||||
secrets_file_path() {
|
||||
echo "$(script_dir)/nexus-1panel.secrets.sh"
|
||||
}
|
||||
|
||||
load_token_from_git_remote() {
|
||||
local root="${1:-$NEXUS_ROOT}"
|
||||
[[ -n "${NEXUS_GITEA_TOKEN:-}" ]] && return 0
|
||||
[[ -d "${root}/.git" ]] || return 0
|
||||
local url
|
||||
url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
|
||||
if [[ "$url" =~ http://[^:]+:([^@]+)@ ]]; then
|
||||
NEXUS_GITEA_TOKEN="${BASH_REMATCH[1]}"
|
||||
info "已从 git remote 解析 Gitea 令牌"
|
||||
fi
|
||||
}
|
||||
|
||||
load_secrets() {
|
||||
local secrets_file
|
||||
secrets_file="$(secrets_file_path)"
|
||||
if [[ -f "$secrets_file" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "$secrets_file"
|
||||
set +a
|
||||
info "已加载 $secrets_file"
|
||||
fi
|
||||
if [[ -f "$NEXUS_DEPLOY_ENV" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "$NEXUS_DEPLOY_ENV"
|
||||
set +a
|
||||
info "已加载 $NEXUS_DEPLOY_ENV"
|
||||
fi
|
||||
load_token_from_git_remote "$NEXUS_ROOT"
|
||||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
info "全新安装:忽略 secrets 中的 NEXUS 密钥,将为本机自动生成"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_profile() {
|
||||
if [[ -n "$CUSTOM_CPUS" && -n "$CUSTOM_MEM_GB" ]]; then
|
||||
local c m
|
||||
c="$CUSTOM_CPUS"
|
||||
m="$CUSTOM_MEM_GB"
|
||||
if [[ "$c" -le 1 && "$m" -le 4 ]]; then
|
||||
NEXUS_PROFILE="1c4g"
|
||||
elif [[ "$c" -le 2 && "$m" -le 8 ]]; then
|
||||
NEXUS_PROFILE="2c8g"
|
||||
else
|
||||
NEXUS_PROFILE="4c16g"
|
||||
fi
|
||||
info "规格 ${c}C${m}G → 档位 ${NEXUS_PROFILE}"
|
||||
return 0
|
||||
fi
|
||||
case "$NEXUS_PROFILE" in
|
||||
1c4g|2c8g|4c16g) ;;
|
||||
*)
|
||||
error "未知档位: $NEXUS_PROFILE(可用: 1c4g, 2c8g, 4c16g)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
profile_env_file() {
|
||||
local root="$1"
|
||||
echo "$root/docker/profiles/${NEXUS_PROFILE}.env"
|
||||
}
|
||||
|
||||
save_host_profile() {
|
||||
local root="$1"
|
||||
echo "$NEXUS_PROFILE" >"$root/$HOST_PROFILE_FILE"
|
||||
}
|
||||
|
||||
load_saved_profile() {
|
||||
local root="$1"
|
||||
if [[ -f "$root/$HOST_PROFILE_FILE" ]]; then
|
||||
NEXUS_PROFILE="$(tr -d '\r\n' <"$root/$HOST_PROFILE_FILE")"
|
||||
info "使用已保存档位: $NEXUS_PROFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_pool_to_env_prod() {
|
||||
local root="$1" prof="$2"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
local pool overflow
|
||||
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
|
||||
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
|
||||
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
|
||||
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
local root="$1"
|
||||
shift
|
||||
local prof
|
||||
prof="$(profile_env_file "$root")"
|
||||
if [[ ! -f "$prof" ]]; then
|
||||
error "缺少档位文件: $prof"
|
||||
exit 1
|
||||
fi
|
||||
cd "$root"
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
}
|
||||
|
||||
rand_secret() {
|
||||
openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||
}
|
||||
|
||||
rand_pass() {
|
||||
openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||||
}
|
||||
|
||||
read_env_val() {
|
||||
local file="$1" key="$2"
|
||||
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
parse_gitea_endpoint() {
|
||||
local hp="${NEXUS_GITEA_HOST}"
|
||||
GITEA_HOST="${hp%%:*}"
|
||||
GITEA_PORT="${hp##*:}"
|
||||
if [[ "$GITEA_HOST" == "$GITEA_PORT" || -z "$GITEA_PORT" ]]; then
|
||||
GITEA_PORT=3000
|
||||
fi
|
||||
}
|
||||
|
||||
nexus_publish_port() {
|
||||
local root="${1:-}"
|
||||
local p="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
if [[ -n "$root" && -f "$root/$ENV_PROD" ]]; then
|
||||
local v
|
||||
v="$(read_env_val "$root/$ENV_PROD" NEXUS_PUBLISH_PORT)"
|
||||
[[ -n "$v" ]] && p="$v"
|
||||
fi
|
||||
echo "$p"
|
||||
}
|
||||
|
||||
tcp_reachable() {
|
||||
local host="$1" port="$2" t="${3:-3}"
|
||||
if command -v nc >/dev/null 2>&1; then
|
||||
nc -z -w "$t" "$host" "$port" 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$t" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
|
||||
&& { exec 3<&- 3>&-; return 0; }
|
||||
else
|
||||
bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
|
||||
&& { exec 3<&- 3>&-; return 0; }
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
local_port_listening() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tlnH 2>/dev/null | grep -qE ":${port}[[:space:]]"
|
||||
return $?
|
||||
fi
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
|
||||
return $?
|
||||
fi
|
||||
curl -sf --max-time 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
firewall_may_block_port() {
|
||||
local port="$1"
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
local st
|
||||
st="$(ufw status 2>/dev/null | head -1 || true)"
|
||||
if [[ "$st" == *"active"* ]]; then
|
||||
ufw status 2>/dev/null | grep -qE "${port}/tcp[[:space:]]+ALLOW" && return 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qi running; then
|
||||
firewall-cmd --list-ports 2>/dev/null | grep -q "${port}/tcp" && return 1
|
||||
firewall-cmd --list-services 2>/dev/null | grep -qE '^(http|https)$' \
|
||||
&& [[ "$port" == "80" || "$port" == "443" ]] && return 1
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
remind_firewall() {
|
||||
local port="$1" label="$2"
|
||||
if firewall_may_block_port "$port"; then
|
||||
warn "防火墙可能未放行 ${port}/tcp(${label})"
|
||||
warn " 本机: ufw allow ${port}/tcp 或 1Panel → 主机 → 防火墙"
|
||||
warn " 云厂商控制台: 安全组入站放行 ${port}/tcp"
|
||||
fi
|
||||
}
|
||||
|
||||
check_ports_preflight() {
|
||||
local root="${1:-$NEXUS_ROOT}"
|
||||
local nport issues=0
|
||||
parse_gitea_endpoint
|
||||
nport="$(nexus_publish_port "$root")"
|
||||
|
||||
step "检查端口与防火墙..."
|
||||
if tcp_reachable "$GITEA_HOST" "$GITEA_PORT"; then
|
||||
info "Gitea ${GITEA_HOST}:${GITEA_PORT} — 可达(git 拉取)"
|
||||
else
|
||||
warn "Gitea ${GITEA_HOST}:${GITEA_PORT} — 无法连接,clone/pull 会失败"
|
||||
warn " 请确认 Gitea 在线,且本机出站与该端口未被防火墙/安全组拦截"
|
||||
issues=$((issues + 1))
|
||||
fi
|
||||
|
||||
if local_port_listening "$nport"; then
|
||||
info "127.0.0.1:${nport} — 已有监听(可能为旧 Nexus 实例)"
|
||||
else
|
||||
info "127.0.0.1:${nport} — 尚未监听(安装后将由 Compose 绑定)"
|
||||
fi
|
||||
|
||||
for spec in "80:HTTP" "443:HTTPS"; do
|
||||
local p="${spec%%:*}" label="${spec##*:}"
|
||||
if local_port_listening "$p"; then
|
||||
info "本机 :${p} — 已监听(${label} / 1Panel)"
|
||||
else
|
||||
warn "本机 :${p} — 未监听,外网 ${label} 暂不可用"
|
||||
warn " 安装完成后: 1Panel → 网站 → 反代 http://127.0.0.1:${nport} 并申请 SSL"
|
||||
issues=$((issues + 1))
|
||||
fi
|
||||
remind_firewall "$p" "${label} 公网"
|
||||
done
|
||||
|
||||
remind_firewall 22 "SSH(远程维护)"
|
||||
|
||||
if [[ "$issues" -gt 0 ]]; then
|
||||
warn "共 ${issues} 项端口提醒(可继续安装,但请先处理 Gitea 不可达问题)"
|
||||
else
|
||||
info "端口预检通过"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_ports_post() {
|
||||
local root="$1"
|
||||
local nport health_ext
|
||||
nport="$(nexus_publish_port "$root")"
|
||||
|
||||
step "检查服务端口..."
|
||||
if local_port_listening "$nport"; then
|
||||
info "Nexus 127.0.0.1:${nport} — 已监听"
|
||||
else
|
||||
warn "127.0.0.1:${nport} — 未监听,请执行: cd $root && docker compose ... ps"
|
||||
fi
|
||||
|
||||
if local_port_listening 443; then
|
||||
health_ext="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${NEXUS_DOMAIN}/health" 2>/dev/null || echo "000")"
|
||||
if [[ "$health_ext" == "200" ]]; then
|
||||
info "https://${NEXUS_DOMAIN}/health — 外网可达 (HTTP 200)"
|
||||
else
|
||||
warn "https://${NEXUS_DOMAIN}/health — 返回 HTTP ${health_ext}"
|
||||
warn " 若外网打不开: 1Panel 反代是否指向 127.0.0.1:${nport},安全组是否放行 443"
|
||||
fi
|
||||
else
|
||||
warn "本机 443 未监听 — 请在 1Panel 创建站点并启用 HTTPS,否则浏览器无法访问 ${NEXUS_DOMAIN}"
|
||||
remind_firewall 443 "HTTPS"
|
||||
fi
|
||||
|
||||
if ! local_port_listening 80; then
|
||||
info "本机 80 未监听(仅 HTTPS 时正常)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_ports() {
|
||||
require_root
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
if [[ -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请切换到 root 执行(su -),不要用 sudo 包一层"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_token() {
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
info "未配置 Gitea 令牌,按公共仓库匿名 clone/pull"
|
||||
}
|
||||
|
||||
git_remote_url() {
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_packages() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl openssl ca-certificates >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_docker() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)"
|
||||
return 0
|
||||
fi
|
||||
error "未检测到 docker compose。请在 1Panel → 容器 安装 Docker。"
|
||||
exit 1
|
||||
}
|
||||
|
||||
clone_or_pull() {
|
||||
local url
|
||||
url="$(git_remote_url)"
|
||||
|
||||
if [[ "$SKIP_CLONE" == true ]]; then
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
error "--skip-clone 但 $NEXUS_ROOT 不是 git 仓库"
|
||||
exit 1
|
||||
fi
|
||||
info "在 $NEXUS_ROOT 执行 git pull..."
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
if [[ -d "$NEXUS_ROOT/.git" ]]; then
|
||||
info "仓库已存在,同步 origin/${GIT_BRANCH}..."
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
|
||||
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
info "克隆 Nexus → $NEXUS_ROOT ..."
|
||||
git clone "$url" "$NEXUS_ROOT"
|
||||
git -C "$NEXUS_ROOT" checkout "$GIT_BRANCH" 2>/dev/null || true
|
||||
}
|
||||
|
||||
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"
|
||||
} >"$f"
|
||||
chmod 600 "$f"
|
||||
echo "$f"
|
||||
}
|
||||
|
||||
write_env_prod() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local example="$root/docker/.env.prod.example"
|
||||
local gen="$root/scripts/generate_nexus_secrets.py"
|
||||
local prof backup tmp
|
||||
|
||||
if [[ ! -f "$example" ]]; then
|
||||
error "缺少 $example"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$gen" ]]; then
|
||||
error "缺少 $gen"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
warn "$env_file 已存在,仅更新域名与连接池"
|
||||
sed -i "s|^NEXUS_CORS_ORIGINS=.*|NEXUS_CORS_ORIGINS=https://${NEXUS_DOMAIN}|" "$env_file"
|
||||
sed -i "s|^NEXUS_API_BASE_URL=.*|NEXUS_API_BASE_URL=https://${NEXUS_DOMAIN}|" "$env_file"
|
||||
apply_pool_to_env_prod "$root" "$(profile_env_file "$root")"
|
||||
chmod 600 "$env_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
prof="$(profile_env_file "$root")"
|
||||
backup="/root/.nexus-install-secrets-$(date +%Y%m%d%H%M%S).env"
|
||||
|
||||
if [[ -n "$FROM_ENV" && -f "$FROM_ENV" ]]; then
|
||||
info "迁机:从 $FROM_ENV 导入密钥..."
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
--domain "$NEXUS_DOMAIN" \
|
||||
--profile-env "$prof" \
|
||||
--from-env "$FROM_ENV" \
|
||||
--backup "$backup"
|
||||
elif [[ "$FRESH_INSTALL" == true ]]; then
|
||||
info "全新安装:为本机生成唯一 MYSQL / NEXUS 密钥..."
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
--domain "$NEXUS_DOMAIN" \
|
||||
--profile-env "$prof" \
|
||||
--fresh \
|
||||
--wizard-pending \
|
||||
--backup "$backup"
|
||||
info "密钥备份: $backup(仅本机,勿复制到其他服务器)"
|
||||
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
|
||||
elif [[ "$REUSE_SECRETS" == true && -n "${NEXUS_SECRET_KEY:-}" && -n "${NEXUS_API_KEY:-}" && -n "${NEXUS_ENCRYPTION_KEY:-}" ]]; then
|
||||
warn "使用 secrets 中的 NEXUS 密钥(仅迁机/复用,新机勿用 --reuse-secrets)"
|
||||
tmp="$(write_temp_secrets_env)"
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
--domain "$NEXUS_DOMAIN" \
|
||||
--profile-env "$prof" \
|
||||
--from-env "$tmp" \
|
||||
--backup "$backup"
|
||||
rm -f "$tmp"
|
||||
else
|
||||
info "未迁机导入:为本机生成唯一密钥..."
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
--domain "$NEXUS_DOMAIN" \
|
||||
--profile-env "$prof" \
|
||||
--fresh \
|
||||
--wizard-pending \
|
||||
--backup "$backup"
|
||||
info "密钥备份: $backup"
|
||||
if [[ "$FRESH_INSTALL" != true ]]; then
|
||||
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod 600 "$env_file"
|
||||
info "已写入 $env_file"
|
||||
}
|
||||
|
||||
compose_up() {
|
||||
local root="$1"
|
||||
save_host_profile "$root"
|
||||
info "档位: ${NEXUS_PROFILE}(MySQL/Nexus 内存与连接池已套用;Redis 请自行安装)"
|
||||
info "构建并启动(首次约 10~20 分钟)..."
|
||||
compose_cmd "$root" up -d --build
|
||||
}
|
||||
|
||||
verify_install_wizard() {
|
||||
local root="$1"
|
||||
local code
|
||||
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
|
||||
info " /app/install.html → HTTP 200"
|
||||
return 0
|
||||
fi
|
||||
warn " /app/install.html → HTTP $code(尝试同步静态文件…)"
|
||||
if [[ -x "$root/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||||
NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
|
||||
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
|
||||
info " /app/install.html → HTTP 200(已同步)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
warn " 请重建镜像: cd $root && docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod build --no-cache nexus"
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_health() {
|
||||
local root="$1" title="${2:-安装}"
|
||||
local i health spa
|
||||
info "等待服务就绪..."
|
||||
for i in $(seq 1 60); do
|
||||
health=$(curl -sf "http://127.0.0.1:8600/health" 2>/dev/null || true)
|
||||
if [[ "$health" == "ok" ]]; then
|
||||
spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
verify_install_wizard "$root" || true
|
||||
echo ""
|
||||
info "═══ ${title}完成 ═══"
|
||||
info " 档位: ${NEXUS_PROFILE}"
|
||||
info " /health → ok"
|
||||
info " /app/ → HTTP $spa"
|
||||
info " 目录: $root"
|
||||
if [[ -d "$root/.git" ]]; then
|
||||
info " 版本: $(git -C "$root" log -1 --oneline)"
|
||||
fi
|
||||
echo ""
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:8600 域名: ${NEXUS_DOMAIN}"
|
||||
info "配置示例: $root/deploy/1panel/openresty-nexus.conf.example"
|
||||
if [[ "$FRESH_INSTALL" == true ]] || ! grep -qE '^NEXUS_SECRET_KEY=.+$' "$root/$ENV_PROD" 2>/dev/null; then
|
||||
info "浏览器: https://${NEXUS_DOMAIN}/app/install.html"
|
||||
else
|
||||
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)"
|
||||
check_ports_post "$root"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
error "健康检查超时"
|
||||
check_ports_post "$root" || true
|
||||
compose_cmd "$root" logs --tail=80 nexus || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
ensure_repo() {
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
error "缺少 $NEXUS_ROOT/$ENV_PROD"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
git_fetch() {
|
||||
local url
|
||||
url="$(git_remote_url)"
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
|
||||
}
|
||||
|
||||
commits_behind() {
|
||||
git -C "$NEXUS_ROOT" rev-list "HEAD..origin/${GIT_BRANCH}" --count 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
show_pending() {
|
||||
local behind
|
||||
behind="$(commits_behind)"
|
||||
info "当前: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
|
||||
info "远程: $(git -C "$NEXUS_ROOT" log -1 --oneline "origin/${GIT_BRANCH}")"
|
||||
if [[ "$behind" == "0" ]]; then
|
||||
info "已是最新。"
|
||||
return 1
|
||||
fi
|
||||
warn "落后 ${behind} 个提交:"
|
||||
git -C "$NEXUS_ROOT" log --oneline "HEAD..origin/${GIT_BRANCH}"
|
||||
return 0
|
||||
}
|
||||
|
||||
backup_mysql() {
|
||||
local root="$1" ts file mysql_root
|
||||
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,跳过备份"
|
||||
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
|
||||
error "mysqldump 失败,已中止"
|
||||
rm -f "$file"
|
||||
exit 1
|
||||
fi
|
||||
gzip -f "$file"
|
||||
info "备份: ${file}.gz"
|
||||
}
|
||||
|
||||
git_upgrade() {
|
||||
local before after
|
||||
before="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
|
||||
step "同步 origin/${GIT_BRANCH}"
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
|
||||
after="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
|
||||
info "代码: ${before:0:7} → ${after:0:7}"
|
||||
}
|
||||
|
||||
cmd_init_token() {
|
||||
require_root
|
||||
local tok="${1:-}"
|
||||
if [[ -z "$tok" ]]; then
|
||||
error "用法: $0 init-token <Gitea令牌>"
|
||||
exit 1
|
||||
fi
|
||||
printf 'NEXUS_GITEA_TOKEN=%s\n' "$tok" >"$NEXUS_DEPLOY_ENV"
|
||||
chmod 600 "$NEXUS_DEPLOY_ENV"
|
||||
info "已写入 $NEXUS_DEPLOY_ENV"
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 1Panel 一键安装"
|
||||
echo " 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
require_root
|
||||
require_token
|
||||
resolve_profile
|
||||
ensure_packages
|
||||
ensure_docker
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
clone_or_pull
|
||||
write_env_prod "$NEXUS_ROOT"
|
||||
compose_up "$NEXUS_ROOT"
|
||||
verify_health "$NEXUS_ROOT" "安装"
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 1Panel 一键升级"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
require_root
|
||||
require_token
|
||||
ensure_docker
|
||||
ensure_repo
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
resolve_profile
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
git_fetch
|
||||
|
||||
if ! show_pending; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$CHECK_ONLY" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
info "dry-run 结束。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
git_upgrade
|
||||
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
|
||||
if [[ "$PRUNE_IMAGES" == true ]]; then
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
fi
|
||||
verify_health "$NEXUS_ROOT" "升级"
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
require_root
|
||||
require_token
|
||||
ensure_repo
|
||||
git_fetch
|
||||
show_pending || true
|
||||
}
|
||||
|
||||
parse_common_install() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--skip-clone) SKIP_CLONE=true; shift ;;
|
||||
--fresh) FRESH_INSTALL=true; shift ;;
|
||||
--reuse-secrets) REUSE_SECRETS=true; shift ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||||
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
|
||||
--from-env) FROM_ENV="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--no-backup) NO_BACKUP=true; shift ;;
|
||||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
*)
|
||||
error "未知参数: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
parse_common_upgrade() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--branch) GIT_BRANCH="$2"; shift 2 ;;
|
||||
--no-backup) NO_BACKUP=true; shift ;;
|
||||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--check) CHECK_ONLY=true; shift ;;
|
||||
*)
|
||||
error "未知参数: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-install}"
|
||||
shift || true
|
||||
|
||||
load_secrets
|
||||
|
||||
case "$cmd" in
|
||||
install)
|
||||
parse_common_install "$@"
|
||||
cmd_install
|
||||
;;
|
||||
upgrade)
|
||||
parse_common_upgrade "$@"
|
||||
cmd_upgrade
|
||||
;;
|
||||
check)
|
||||
CHECK_ONLY=true
|
||||
parse_common_upgrade "$@"
|
||||
cmd_check
|
||||
;;
|
||||
init-token)
|
||||
cmd_init_token "${1:-}"
|
||||
;;
|
||||
ports)
|
||||
cmd_ports
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
error "未知子命令: $cmd"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus NX — 一键安装 + 上帝菜单(1Panel Docker 生产栈)
|
||||
#
|
||||
# bash deploy/nx 交互菜单
|
||||
# bash deploy/nx install 同菜单「安装」
|
||||
# bash deploy/nx god 上帝菜单(运维)
|
||||
# nx 安装后链到 /usr/local/bin/nx
|
||||
#
|
||||
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
|
||||
source "${NX_DIR}/nexus-1panel.sh"
|
||||
|
||||
pause_enter() {
|
||||
echo ""
|
||||
read -r -p "按 Enter 继续..."
|
||||
}
|
||||
|
||||
clear_screen() {
|
||||
if [[ -t 1 ]]; then
|
||||
clear 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
is_stack_installed() {
|
||||
[[ -d "${NEXUS_ROOT}/.git" && -f "${NEXUS_ROOT}/${ENV_PROD}" ]]
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if is_stack_installed && docker compose version >/dev/null 2>&1; then
|
||||
local prof q
|
||||
prof="$(profile_env_file "$NEXUS_ROOT")"
|
||||
if [[ -f "$prof" && -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
q="$(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" \
|
||||
--env-file "$ENV_PROD" --env-file "$prof" ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
NX_RUNTIME="docker"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
NX_RUNTIME="supervisor"
|
||||
return 0
|
||||
fi
|
||||
NX_RUNTIME="none"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
menu_header() {
|
||||
local title="$1"
|
||||
clear_screen
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Nexus NX — ${title}"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
|
||||
if is_stack_installed; then
|
||||
detect_runtime
|
||||
echo "║ 运行: ${NX_RUNTIME} 档位: $(cat "${NEXUS_ROOT}/${HOST_PROFILE_FILE}" 2>/dev/null || echo "${NEXUS_PROFILE}")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
echo "║ 版本: $(git -C "${NEXUS_ROOT}" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
fi
|
||||
else
|
||||
echo "║ 状态: 未安装"
|
||||
fi
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_install() {
|
||||
local profile="${1:-2c8g}" extra="${2:-}"
|
||||
NEXUS_PROFILE="$profile"
|
||||
SKIP_CLONE=false
|
||||
FRESH_INSTALL=false
|
||||
NO_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
case "$extra" in
|
||||
skip-clone) SKIP_CLONE=true ;;
|
||||
fresh) FRESH_INSTALL=true ;;
|
||||
from-env) FROM_ENV="${NEXUS_FROM_ENV}" ;;
|
||||
esac
|
||||
load_secrets
|
||||
cmd_install
|
||||
install_nx_cli
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_install() {
|
||||
while true; do
|
||||
menu_header "安装向导"
|
||||
cat <<'EOF'
|
||||
[1] 一键安装(默认 2核8G / 2c8g)
|
||||
[2] 安装 — 4核16G(4c16g)
|
||||
[3] 安装 — 1核4G(1c4g)
|
||||
[4] 自定义规格(输入 CPU 与内存 GB)
|
||||
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone)
|
||||
[6] 全新安装(install-nexus-fresh.sh → /app/install.html)
|
||||
[7] 检查端口 / 防火墙(不安装)
|
||||
[8] 写入 Gitea 令牌(init-token)
|
||||
[9] 返回主菜单
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) run_install 2c8g "" ;;
|
||||
2) run_install 4c16g "" ;;
|
||||
3) run_install 1c4g "" ;;
|
||||
4)
|
||||
read -r -p "CPU 核数: " cpus
|
||||
read -r -p "内存 GB: " mem
|
||||
CUSTOM_CPUS="$cpus"
|
||||
CUSTOM_MEM_GB="$mem"
|
||||
run_install 2c8g ""
|
||||
;;
|
||||
5) run_install 2c8g skip-clone ;;
|
||||
6)
|
||||
if [[ -x "${NX_DIR}/install-nexus-fresh.sh" ]]; then
|
||||
bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
else
|
||||
run_install 2c8g fresh
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
require_root
|
||||
load_secrets
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
if is_stack_installed; then
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
read -r -p "Gitea Access Token: " tok
|
||||
cmd_init_token "$tok"
|
||||
pause_enter
|
||||
;;
|
||||
9) return 0 ;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项" ; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
god_restart_nexus() {
|
||||
step "重启 Nexus(Python)..."
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" restart nexus
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl restart nexus
|
||||
else
|
||||
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 "健康检查未通过,请查看日志"
|
||||
}
|
||||
|
||||
god_restart_stack() {
|
||||
step "重启全部 Compose 服务..."
|
||||
compose_cmd "$NEXUS_ROOT" restart
|
||||
sleep 5
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
god_stop_stack() {
|
||||
warn "将停止 MySQL / Redis / Nexus 全部容器"
|
||||
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
|
||||
verify_health "$NEXUS_ROOT" "启动" || true
|
||||
}
|
||||
|
||||
god_logs() {
|
||||
local svc="${1:-nexus}"
|
||||
echo ""
|
||||
read -r -p "跟踪日志行数 [80]: " n
|
||||
n="${n:-80}"
|
||||
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
|
||||
}
|
||||
|
||||
god_status() {
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" ps
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl status nexus
|
||||
fi
|
||||
echo ""
|
||||
local port h
|
||||
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
|
||||
}
|
||||
|
||||
god_backup_now() {
|
||||
NO_BACKUP=false
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
}
|
||||
|
||||
god_upgrade_interactive() {
|
||||
PRUNE_IMAGES=false
|
||||
NO_BACKUP=false
|
||||
read -r -p "跳过 MySQL 备份? [y/N]: " nb
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && NO_BACKUP=true
|
||||
read -r -p "升级后清理悬空镜像? [y/N]: " pr
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && PRUNE_IMAGES=true
|
||||
cmd_upgrade
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_god() {
|
||||
if ! is_stack_installed; then
|
||||
warn "尚未安装,请先使用安装向导"
|
||||
pause_enter
|
||||
return 0
|
||||
fi
|
||||
require_root
|
||||
load_secrets
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
|
||||
while true; do
|
||||
menu_header "上帝菜单 · 运维"
|
||||
cat <<EOF
|
||||
── 服务 ──
|
||||
[1] 重启 Nexus(Python 应用)
|
||||
[2] 重启全部容器(mysql + redis + nexus)
|
||||
[3] 启动 Compose 栈
|
||||
[4] 停止 Compose 栈
|
||||
[5] 重建 Nexus 镜像并启动(up -d --build nexus)
|
||||
|
||||
── 观测 ──
|
||||
[6] 状态 + 健康检查
|
||||
[7] 查看 Nexus 日志(跟踪)
|
||||
[8] 查看 MySQL / Redis 日志
|
||||
[9] 端口 / 防火墙检查
|
||||
|
||||
── 发布 ──
|
||||
[a] 检查 Git 更新
|
||||
[b] 一键升级(备份 + pull + 重建)
|
||||
[c] 立即备份 MySQL
|
||||
|
||||
── 其他 ──
|
||||
[d] 进入 Nexus 容器 shell
|
||||
[e] 打开 1Panel 反代说明
|
||||
[0] 返回主菜单
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) god_restart_nexus; pause_enter ;;
|
||||
2) god_restart_stack; pause_enter ;;
|
||||
3) god_start_stack; pause_enter ;;
|
||||
4) god_stop_stack; pause_enter ;;
|
||||
5)
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build nexus
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
6) god_status; pause_enter ;;
|
||||
7) god_logs nexus ;;
|
||||
8)
|
||||
echo "1) mysql 2) redis"
|
||||
read -r -p "选: " s
|
||||
[[ "$s" == "1" ]] && god_logs mysql || god_logs redis
|
||||
;;
|
||||
9)
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B) god_upgrade_interactive ;;
|
||||
c|C) god_backup_now; pause_enter ;;
|
||||
d)
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
e)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例配置: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
menu_main() {
|
||||
load_secrets
|
||||
while true; do
|
||||
menu_header "主菜单"
|
||||
if is_stack_installed; then
|
||||
echo " 提示: 安装完成后请用 [2] 上帝菜单 重启 Python / 升级 / 看日志"
|
||||
echo ""
|
||||
fi
|
||||
cat <<'EOF'
|
||||
[1] 安装向导(新机 / 迁机)
|
||||
[2] 上帝菜单(重启 Python · 升级 · 日志 · 备份)
|
||||
[3] 快速健康检查
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) menu_install ;;
|
||||
2) menu_god ;;
|
||||
3)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
god_status
|
||||
else
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
nx_main() {
|
||||
local sub="${1:-}"
|
||||
load_secrets
|
||||
case "$sub" in
|
||||
""|menu) menu_main ;;
|
||||
install) menu_install ;;
|
||||
god|ops|admin) menu_god ;;
|
||||
install-fresh|fresh)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
|
||||
;;
|
||||
install-auto|auto)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" skip-clone
|
||||
else
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" ""
|
||||
fi
|
||||
;;
|
||||
health)
|
||||
require_root
|
||||
is_stack_installed && god_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
用法: nx [子命令]
|
||||
|
||||
(无) 交互主菜单
|
||||
install 安装向导
|
||||
install-fresh 全新安装(1Panel 风格,/app/install.html)
|
||||
install-auto 非交互安装已有密钥(2c8g)
|
||||
god 上帝菜单(重启/升级/日志)
|
||||
health 健康检查
|
||||
|
||||
底层仍可使用: bash deploy/nexus-1panel.sh install|upgrade|check|ports
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
# 兼容 nexus-1panel.sh 子命令
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
|
||||
nx_main "$@"
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 公共仓库一键安装入口(参考 1Panel quick_start.sh)
|
||||
#
|
||||
# root 登录后一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | 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
|
||||
#
|
||||
# 可选参数会传给 install-nexus-fresh.sh,例如:
|
||||
# bash /tmp/quick-install.sh --profile 4c16g
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 公共仓库一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/quick-install.sh" | bash
|
||||
|
||||
可选参数: --profile 4c16g --domain 你的域名 --skip-1panel-check
|
||||
EOF
|
||||
}
|
||||
|
||||
validate_script_file() {
|
||||
local f="$1"
|
||||
if [[ ! -f "$f" ]] || ! head -1 "$f" | grep -q '^#!/'; then
|
||||
return 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$f" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
download_script() {
|
||||
local name="$1" dest="$2"
|
||||
local url base
|
||||
for base in \
|
||||
"${NEXUS_RAW_BASE}" \
|
||||
"http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && validate_script_file "$dest"; then
|
||||
info "已下载 ${name}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_via_git() {
|
||||
local url="http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
step "改用 git clone..."
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --depth 1 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}" 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" pull
|
||||
else
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
git clone --depth 1 -b "$NEXUS_GIT_BRANCH" "$url" "$NEXUS_ROOT"
|
||||
fi
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
exec bash "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" --skip-clone "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行,不要用 sudo 包一层)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 公共仓库一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
esac
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
if download_script "install-nexus-fresh.sh" "$tmp"; then
|
||||
chmod +x "$tmp"
|
||||
exec bash "$tmp" "$@"
|
||||
fi
|
||||
|
||||
step "raw 下载失败,尝试 git clone..."
|
||||
run_via_git "$@"
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync install wizard static files into the running Nexus prod container.
|
||||
# Primary fix: Dockerfile.prod bakes these into the image on build.
|
||||
# This script is a safety net after upgrade if an old image is still running.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
|
||||
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; }
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
sync_file() {
|
||||
local src="$1" dest="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
error "缺少源文件: $src"
|
||||
exit 1
|
||||
fi
|
||||
docker exec "$CONTAINER" mkdir -p "$(dirname "$dest")"
|
||||
docker cp "$src" "${CONTAINER}:${dest}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="$NEXUS_ROOT/web/app"
|
||||
if [[ ! -f "$app/install.html" ]]; then
|
||||
error "仓库中无 $app/install.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
resolve_container
|
||||
info "同步安装向导静态文件..."
|
||||
sync_file "$app/install.html" /app/web/app/install.html
|
||||
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
|
||||
sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js
|
||||
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
|
||||
sync_file "$app/vendor/alpinejs.min.js" /app/web/app/vendor/alpinejs.min.js
|
||||
|
||||
local install_py="$NEXUS_ROOT/server/api/install.py"
|
||||
if [[ -f "$install_py" ]]; then
|
||||
info "同步 install.py(Docker Redis env-check 修复)..."
|
||||
sync_file "$install_py" /app/server/api/install.py
|
||||
info "重启 Nexus 容器以加载 Python 变更..."
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf http://127.0.0.1:8600/health >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
warn "未找到 $install_py,跳过 Python 热同步"
|
||||
fi
|
||||
|
||||
local code
|
||||
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
|
||||
info "/app/install.html → HTTP 200"
|
||||
else
|
||||
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 8600 监听)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# 兼容入口 — 请优先使用 deploy/nexus-1panel.sh
|
||||
exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/nexus-1panel.sh" upgrade "$@"
|
||||
@@ -1,14 +1,18 @@
|
||||
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||
# Migrate NEXUS_* secrets from Baota /www/wwwroot/.../.env UNCHANGED.
|
||||
# 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
|
||||
|
||||
# Required — copy from production .env (do NOT regenerate)
|
||||
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
|
||||
NEXUS_SECRET_KEY=
|
||||
NEXUS_API_KEY=
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
# 1 = entrypoint must not write /app/.env until install wizard step 3
|
||||
NEXUS_INSTALL_WIZARD_PENDING=0
|
||||
|
||||
# Public URL
|
||||
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
@@ -17,5 +21,9 @@ NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# Redis 不在 Docker 栈内 — 请在宿主机或 1Panel 应用商店自行安装
|
||||
# Nexus 容器通过 host.docker.internal 访问宿主机 Redis(见 docker-compose.prod.yml)
|
||||
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||
|
||||
# Optional
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
|
||||
+5
-3
@@ -44,8 +44,8 @@ docker compose build --no-cache nexus
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| nexus | 8600 | FastAPI + 静态 `/app/` |
|
||||
| mysql | 3306 | 数据库 `nexus` |
|
||||
| redis | 6379 | 仅容器内网(未映射到主机) |
|
||||
| mysql | 3306 | 数据库 `nexus`(Compose 内) |
|
||||
| redis | — | **不在 Compose 内**;宿主机/1Panel 自建,默认 `host.docker.internal:6379` |
|
||||
|
||||
## 生产注意
|
||||
|
||||
@@ -60,9 +60,11 @@ cp docker/.env.prod.example docker/.env.prod # 从宝塔 .env 迁入密钥
|
||||
docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up -d --build
|
||||
```
|
||||
|
||||
- 镜像:`Dockerfile.prod`(含 `npm ci` + `vite build`)
|
||||
- 镜像:`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`
|
||||
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
|
||||
- 一键安装:`bash deploy/install-1panel-docker.sh`
|
||||
- 一键升级:`bash deploy/upgrade-1panel-docker.sh`(`--check` 仅查更新)
|
||||
|
||||
详见 `docs/design/specs/2026-06-03-docker-design.md`。
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Nexus 6.0 — production Compose (1Panel / bare Docker host)
|
||||
# Tuned for 2 vCPU / 8 GiB — see docker/mysql-prod-2c8g.cnf
|
||||
# Resource profile: docker/profiles/{1c4g,2c8g,4c16g}.env — see deploy/nexus-1panel.sh
|
||||
#
|
||||
# Usage:
|
||||
# cp docker/.env.prod.example docker/.env.prod # fill secrets from Baota .env
|
||||
# docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up -d --build
|
||||
# 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
|
||||
#
|
||||
# OpenResty on host proxies https://api.synaglobal.vip → 127.0.0.1:8600
|
||||
|
||||
@@ -20,9 +22,9 @@ services:
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./mysql-prod-2c8g.cnf:/etc/mysql/conf.d/99-nexus-2c8g.cnf:ro
|
||||
mem_limit: 2g
|
||||
cpus: 1.5
|
||||
- ./${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
|
||||
@@ -32,29 +34,6 @@ services:
|
||||
networks:
|
||||
- nexus-internal
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- "yes"
|
||||
- --maxmemory
|
||||
- 256mb
|
||||
- --maxmemory-policy
|
||||
- allkeys-lru
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
mem_limit: 384m
|
||||
cpus: 0.5
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 8
|
||||
networks:
|
||||
- nexus-internal
|
||||
|
||||
nexus:
|
||||
build:
|
||||
context: ..
|
||||
@@ -63,34 +42,34 @@ services:
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
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"
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
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
|
||||
NEXUS_REDIS_URL: redis://redis:6379/0
|
||||
# Redis 不在 Compose 内 — 请在宿主机/1Panel 自建,默认连宿主机 6379
|
||||
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://host.docker.internal:6379/0}
|
||||
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}
|
||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
|
||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:?set NEXUS_ENCRYPTION_KEY}
|
||||
NEXUS_INSTALL_WIZARD_PENDING: ${NEXUS_INSTALL_WIZARD_PENDING:-0}
|
||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:?set NEXUS_API_BASE_URL}
|
||||
NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30}
|
||||
NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20}
|
||||
volumes:
|
||||
- nexus-state:/var/lib/nexus
|
||||
- nexus-web-data:/app/web/data
|
||||
mem_limit: 2g
|
||||
cpus: 1.5
|
||||
mem_limit: ${NEXUS_MEM_LIMIT:-2g}
|
||||
cpus: ${NEXUS_CPUS:-1.5}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
|
||||
interval: 30s
|
||||
@@ -106,6 +85,5 @@ networks:
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
|
||||
+35
-3
@@ -20,6 +20,30 @@ s.close()
|
||||
done
|
||||
}
|
||||
|
||||
wait_tcp_optional() {
|
||||
host="$1"
|
||||
port="$2"
|
||||
tries="${3:-15}"
|
||||
echo "entrypoint: waiting for external Redis ${host}:${port} (max ${tries} tries)..."
|
||||
n=0
|
||||
while [ "$n" -lt "$tries" ]; do
|
||||
if python3 -c "
|
||||
import socket
|
||||
s = socket.socket()
|
||||
s.settimeout(2)
|
||||
s.connect(('${host}', ${port}))
|
||||
s.close()
|
||||
" 2>/dev/null; then
|
||||
echo "entrypoint: Redis ready at ${host}:${port}"
|
||||
return 0
|
||||
fi
|
||||
n=$((n + 1))
|
||||
sleep 2
|
||||
done
|
||||
echo "entrypoint: warn — Redis not reachable at ${host}:${port}; install on host or set NEXUS_REDIS_URL"
|
||||
return 0
|
||||
}
|
||||
|
||||
restore_persisted_env() {
|
||||
if [ -f "${PERSIST_ENV}" ]; then
|
||||
cp "${PERSIST_ENV}" /app/.env
|
||||
@@ -38,6 +62,10 @@ write_env_from_environment() {
|
||||
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-}" = "1" ]; then
|
||||
echo "entrypoint: install wizard pending — skip writing /app/.env"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
{
|
||||
echo "# Generated by docker/entrypoint.sh — do not commit"
|
||||
@@ -60,11 +88,15 @@ trap persist_env_on_exit EXIT INT TERM
|
||||
|
||||
MYSQL_HOST="${MYSQL_HOST:-mysql}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
REDIS_HOST="${REDIS_HOST:-redis}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
|
||||
wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}"
|
||||
wait_tcp "${REDIS_HOST}" "${REDIS_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_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15
|
||||
fi
|
||||
|
||||
restore_persisted_env
|
||||
write_env_from_environment
|
||||
|
||||
+18
-30
@@ -3,47 +3,35 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.generate_nexus_secrets import ( # noqa: E402
|
||||
generate_nexus_secrets,
|
||||
write_env_prod,
|
||||
)
|
||||
|
||||
OUT = ROOT / "docker" / ".env"
|
||||
EXAMPLE = ROOT / "docker" / ".env.example"
|
||||
|
||||
|
||||
def _token(n: int = 32) -> str:
|
||||
return secrets.token_urlsafe(n)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if OUT.exists():
|
||||
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
|
||||
|
||||
mysql_root = _token(24)
|
||||
mysql_pass = _token(24)
|
||||
secret = _token(32)
|
||||
api_key = _token(24)
|
||||
# Fernet-compatible (same as install wizard)
|
||||
enc_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
|
||||
|
||||
text = EXAMPLE.read_text(encoding="utf-8")
|
||||
lines: list[str] = []
|
||||
for line in text.splitlines():
|
||||
if line.startswith("MYSQL_ROOT_PASSWORD="):
|
||||
lines.append(f"MYSQL_ROOT_PASSWORD={mysql_root}")
|
||||
elif line.startswith("MYSQL_PASSWORD="):
|
||||
lines.append(f"MYSQL_PASSWORD={mysql_pass}")
|
||||
elif line.startswith("NEXUS_SECRET_KEY="):
|
||||
lines.append(f"NEXUS_SECRET_KEY={secret}")
|
||||
elif line.startswith("NEXUS_API_KEY="):
|
||||
lines.append(f"NEXUS_API_KEY={api_key}")
|
||||
elif line.startswith("NEXUS_ENCRYPTION_KEY="):
|
||||
lines.append(f"NEXUS_ENCRYPTION_KEY={enc_key}")
|
||||
else:
|
||||
lines.append(line)
|
||||
|
||||
OUT.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
secrets_map = generate_nexus_secrets()
|
||||
write_env_prod(
|
||||
EXAMPLE,
|
||||
OUT,
|
||||
domain="localhost",
|
||||
secrets_map=secrets_map,
|
||||
profile_env=None,
|
||||
wizard_pending=False,
|
||||
)
|
||||
print(f"Wrote {OUT}")
|
||||
print("Next: docker compose up -d --build")
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 512M
|
||||
innodb_log_file_size = 128M
|
||||
|
||||
max_connections = 80
|
||||
|
||||
innodb_read_io_threads = 2
|
||||
innodb_write_io_threads = 2
|
||||
|
||||
table_open_cache = 1000
|
||||
tmp_table_size = 32M
|
||||
max_heap_table_size = 32M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -0,0 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 4096M
|
||||
innodb_log_file_size = 512M
|
||||
|
||||
max_connections = 200
|
||||
|
||||
innodb_read_io_threads = 8
|
||||
innodb_write_io_threads = 8
|
||||
|
||||
table_open_cache = 4000
|
||||
tmp_table_size = 128M
|
||||
max_heap_table_size = 128M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -0,0 +1,8 @@
|
||||
# Host profile: 1 vCPU / 4 GiB (test / small VPS)
|
||||
MYSQL_CNF_FILE=mysql-prod-1c4g.cnf
|
||||
MYSQL_MEM_LIMIT=1g
|
||||
MYSQL_CPUS=1
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
NEXUS_DB_MAX_OVERFLOW=10
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
@@ -0,0 +1,33 @@
|
||||
# Changelog — 1Panel Docker 一键安装脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/install-1panel-docker.sh`:在 1Panel 服务器上自动 clone/pull、生成 `docker/.env.prod`、启动生产 Compose 并健康检查。
|
||||
|
||||
## 动机
|
||||
|
||||
用户刚装好 1Panel,需要减少手工步骤与复制 token 错误。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/install-1panel-docker.sh`
|
||||
- `SECRETS.md` — 用法示例
|
||||
- `docs/design/plans/2026-06-04-1panel-docker-production.md` — 引用脚本
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
export NEXUS_GITEA_TOKEN='...'
|
||||
export NEXUS_DOMAIN='api.synaglobal.vip'
|
||||
bash deploy/install-1panel-docker.sh
|
||||
```
|
||||
|
||||
迁机:`export NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'`
|
||||
|
||||
## 验证
|
||||
|
||||
`bash -n deploy/install-1panel-docker.sh`;服务器执行后 `/health` → `ok`。
|
||||
|
||||
## 回滚
|
||||
|
||||
`docker compose -f docker/docker-compose.prod.yml down`;删除 `/opt/nexus`。
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog — 1Panel Docker 一键升级脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/upgrade-1panel-docker.sh`:检查 Gitea 更新、可选 MySQL 备份、`git reset --hard`、Compose 重建与健康检查。
|
||||
|
||||
## 动机
|
||||
|
||||
私人 Gitea 仓库需在服务器上一键拉取并部署新版本,与安装脚本配套。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/upgrade-1panel-docker.sh`
|
||||
- `deploy/install-1panel-docker.sh` — 注释指向升级脚本
|
||||
- `SECRETS.md` — `/root/.nexus-deploy.env` 与升级命令
|
||||
- `docker/README.md`
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
# 持久化 token(可选)
|
||||
echo 'NEXUS_GITEA_TOKEN=...' > /root/.nexus-deploy.env && chmod 600 /root/.nexus-deploy.env
|
||||
|
||||
bash /opt/nexus/deploy/upgrade-1panel-docker.sh --check
|
||||
bash /opt/nexus/deploy/upgrade-1panel-docker.sh
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
`bash -n deploy/upgrade-1panel-docker.sh`
|
||||
|
||||
## 回滚
|
||||
|
||||
`git reset --hard <旧commit>` + `compose up -d --build`;或恢复 `nexus-before-upgrade-*.sql.gz`。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Changelog: 1Panel 风格全新安装脚本
|
||||
|
||||
**日期**: 2026-06-04
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/install-nexus-fresh.sh`:参考 1Panel `quick_start.sh` 的分阶段检查与安装完成信息框;空库安装后引导 `/app/install.html` 与 1Panel 反代配置。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/install-nexus-fresh.sh` — 主脚本
|
||||
- `deploy/nx` — 菜单项 6 / 子命令 `install-fresh`
|
||||
- `deploy/README-1panel.md`
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
curl -sSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-nexus-fresh.sh" -o /tmp/install-nexus-fresh.sh
|
||||
sudo bash /tmp/install-nexus-fresh.sh
|
||||
sudo bash /tmp/install-nexus-fresh.sh --profile 4c16g
|
||||
```
|
||||
|
||||
或:`bash deploy/nx install-fresh`
|
||||
|
||||
## 与迁机区别
|
||||
|
||||
| 脚本 | 场景 |
|
||||
|------|------|
|
||||
| `install-nexus-fresh.sh` | 新库 + 安装向导 |
|
||||
| `nexus-1panel.sh install --from-env` | 宝塔迁密钥 |
|
||||
|
||||
## 验证
|
||||
|
||||
- `bash -n deploy/install-nexus-fresh.sh`
|
||||
- 1Panel 新机执行后 `/health` 与 `/app/install.html` 可访问
|
||||
@@ -0,0 +1,61 @@
|
||||
# Changelog: 1Panel 统一一键脚本 + 资源档位
|
||||
|
||||
**日期**: 2026-06-04
|
||||
|
||||
## 摘要
|
||||
|
||||
合并 `install-1panel-docker.sh` 与 `upgrade-1panel-docker.sh` 为 `deploy/nexus-1panel.sh`:域名、Gitea、路径等默认已填;支持 `install` / `upgrade` / `check` / `init-token`;可选 `--profile 2c8g|4c16g|1c4g` 或 `--cpus N --mem-gb N` 自动匹配 MySQL/Redis/Nexus 内存与连接池。
|
||||
|
||||
## 动机
|
||||
|
||||
用户要求单脚本、少 export、可按机器核数/内存选档。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nexus-1panel.sh` — 主入口
|
||||
- `deploy/install-1panel-docker.sh`、`deploy/upgrade-1panel-docker.sh` — 转调主脚本
|
||||
- `deploy/nexus-1panel.secrets.sh.example` — 令牌模板(真实令牌用 `init-token` 或 gitignore 的 `nexus-1panel.secrets.sh`)
|
||||
- `docker/profiles/{1c4g,2c8g,4c16g}.env`
|
||||
- `docker/mysql-prod-{1c4g,4c16g}.cnf`
|
||||
- `docker/docker-compose.prod.yml` — 档位变量化
|
||||
|
||||
## 服务器用法
|
||||
|
||||
```bash
|
||||
# 首次(写入令牌,仅需一次)
|
||||
bash deploy/nexus-1panel.sh init-token '你的Gitea令牌'
|
||||
|
||||
# 2核8G(默认)
|
||||
bash deploy/nexus-1panel.sh install
|
||||
|
||||
# 4核16G
|
||||
bash deploy/nexus-1panel.sh install --profile 4c16g
|
||||
|
||||
# 或指定规格自动匹配档位
|
||||
bash deploy/nexus-1panel.sh install --cpus 4 --mem-gb 16
|
||||
|
||||
# 已 clone 在 /opt/nexus
|
||||
cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
|
||||
|
||||
# 升级
|
||||
bash deploy/nexus-1panel.sh upgrade
|
||||
bash deploy/nexus-1panel.sh check
|
||||
```
|
||||
|
||||
## 迁移/重启
|
||||
|
||||
- 改档位后需 `docker compose up -d --build`(install/upgrade 已包含)
|
||||
- MySQL `innodb_buffer_pool` 变更可能需容器重建
|
||||
|
||||
## 后续(同批)
|
||||
|
||||
- 凭据集中在 `deploy/nexus-1panel.secrets.sh`(gitignore);私人仓库可 `git add -f` 后服务器零 export 安装
|
||||
- `install` / `upgrade` 前检测 Gitea 端口可达性;检测本机 80/443/8600 与 UFW/firewalld 放行提醒
|
||||
- 子命令 `ports` 可单独跑端口检查
|
||||
- 安装成功后检测 `https://域名/health` 外网是否可达
|
||||
|
||||
## 验证
|
||||
|
||||
- `bash -n deploy/nexus-1panel.sh`
|
||||
- `bash deploy/nexus-1panel.sh ports`
|
||||
- 服务器:`install --skip-clone` + `curl http://127.0.0.1:8600/health`
|
||||
@@ -0,0 +1,32 @@
|
||||
# Changelog: NX 一键菜单 + 上帝运维面板
|
||||
|
||||
**日期**: 2026-06-04
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/nx` 交互脚本:安装向导(档位/迁机/端口检查)与安装后「上帝菜单」,可重启 Nexus Python 容器、重启全栈、查看日志、升级、MySQL 备份等。
|
||||
|
||||
## 动机
|
||||
|
||||
用户要求单一入口,安装与日常运维(重启 Python 等)全部在菜单内完成。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nx` — 主入口(菜单)
|
||||
- `deploy/nexus-1panel.sh` — 可被 source,CLI 子命令保留
|
||||
- `deploy/install-1panel-docker.sh` — 转调 `nx`
|
||||
- `deploy/README-1panel.md`
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
bash deploy/nx # 主菜单
|
||||
bash deploy/nx god # 上帝菜单
|
||||
bash deploy/nx install-auto # 非交互安装
|
||||
sudo ln -sf /opt/nexus/deploy/nx /usr/local/bin/nx
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
- `bash -n deploy/nx && bash -n deploy/nexus-1panel.sh`
|
||||
- 服务器安装后: `nx` → 上帝菜单 → 重启 Nexus → `/health` 为 ok
|
||||
@@ -0,0 +1,26 @@
|
||||
# Changelog: 公共仓库 quick-install 一键脚本
|
||||
|
||||
**日期**: 2026-06-04
|
||||
|
||||
## 摘要
|
||||
|
||||
仓库改为公共后,新增 `deploy/quick-install.sh`(1Panel 风格 `curl | bash`),匿名 raw / git clone 均可用,无需 Gitea 令牌。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/quick-install.sh` — 主入口
|
||||
- `deploy/install-nexus-fresh.sh` — 公共 clone、全局 `nexus-install`
|
||||
- `deploy/download-install-fresh.sh` — 简化
|
||||
- `deploy/nexus-1panel.sh` — 令牌可选
|
||||
- `deploy/README-1panel.md`、`SECRETS.md`
|
||||
|
||||
## 服务器用法
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
- `curl` raw 返回 `#!/usr/bin/env bash`
|
||||
- `bash -n deploy/quick-install.sh`
|
||||
@@ -0,0 +1,31 @@
|
||||
# 2026-06-05 — Docker 栈移除内置 Redis
|
||||
|
||||
## 摘要
|
||||
|
||||
按用户要求,一键安装脚本 / `docker-compose.prod.yml` **不再附带 Redis 容器**。
|
||||
Redis 由用户在宿主机或 1Panel 应用商店自行安装;Nexus 默认连 `host.docker.internal:6379`。
|
||||
|
||||
## 变更
|
||||
|
||||
- `docker/docker-compose.prod.yml` — 删除 `redis` 服务与 `redis-data` 卷
|
||||
- `docker/entrypoint.sh` — 安装向导阶段不等待 Redis;否则有限次等待外置 Redis
|
||||
- `server/api/install.py` — Redis 环境检测改为**不阻塞**(`pass: true`)
|
||||
- `deploy/README-1panel.md` — 补充 Redis 自建说明
|
||||
- `docker/.env.prod.example` — 增加 `NEXUS_REDIS_URL`
|
||||
|
||||
## 迁移(已装旧栈)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
docker stop nexus-prod-redis-1 2>/dev/null; docker rm nexus-prod-redis-1 2>/dev/null
|
||||
# 在宿主机/1Panel 安装 Redis 并监听 6379
|
||||
docker compose -f docker/docker-compose.prod.yml \
|
||||
--env-file docker/.env.prod \
|
||||
--env-file docker/profiles/$(cat docker/.host-profile).env \
|
||||
up -d --build
|
||||
```
|
||||
|
||||
## 安装向导
|
||||
|
||||
- Docker:Redis 主机 `host.docker.internal`,端口 `6379`
|
||||
- 步骤 2 Redis 未连接也可继续
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-06-05 — Gitea 仓库产品说明 README
|
||||
|
||||
## 摘要
|
||||
|
||||
在仓库根目录新增 `README.md`,作为 Gitea 项目页产品说明:项目简介、公共仓库一键安装、NX 上帝菜单、deploy/docker/scripts 脚本索引、迁机与全新安装对照、文档链接。
|
||||
|
||||
## 动机
|
||||
|
||||
Gitea 仓库页原先无根 README(Description 为空),运维无法在仓库首页看到 `curl | bash` 与脚本用法。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `README.md`(新建)
|
||||
- `docs/changelog/2026-06-05-gitea-readme-product.md`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无。仅文档。
|
||||
|
||||
## 验证
|
||||
|
||||
1. 本地打开 `README.md` 链接与脚本路径存在。
|
||||
2. `git push origin main` 后在 <http://66.154.115.8:3000/admin/Nexus> 首页应渲染 README。
|
||||
3. 确认 `raw/branch/main/deploy/quick-install.sh` 可匿名下载(200 + `#!/`)。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 2026-06-05 — install-1panel-docker 一键脚本
|
||||
|
||||
## 摘要
|
||||
|
||||
新增 `deploy/install-1panel-docker.sh`,作为**重装服务器**时的官方一键入口:串联 1Panel 安装提示、Docker Compose 检测/安装、Nexus Docker 栈部署。
|
||||
|
||||
## 动机
|
||||
|
||||
- 原 `install-1panel-docker.sh` 仅为 `nx install` 的兼容转发,名不副实。
|
||||
- 用户重装 VPS 后需要明确的两层入口:全栈 vs 仅 Nexus。
|
||||
- 前次事故:Agent 直接改 `/opt/1panel/apps/` 破坏 OpenResty bind mount,需在文档中禁止。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/install-1panel-docker.sh` — 新建全栈编排脚本
|
||||
- `deploy/README-1panel.md` — 更新安装说明与反代禁令
|
||||
- `deploy/install-nexus-fresh.sh` — 注册 `nexus-1panel-docker` 全局命令
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
# 重装服务器(全栈)
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
|
||||
# 已装 1Panel
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
```
|
||||
|
||||
## 是否需迁移/重启
|
||||
|
||||
- 仅文档与脚本,无运行时变更;push 后新机 curl 即生效。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bash -n deploy/install-1panel-docker.sh`
|
||||
- 逻辑审阅:不写入 `/opt/1panel/apps/`,反代仍由用户在 1Panel UI 配置
|
||||
|
||||
## 补丁(同日)
|
||||
|
||||
- `curl | bash` 下 `BASH_SOURCE[0]` 在 `set -u` 时为 unbound → 入口改为直接 `main "$@"`(`install-1panel-docker.sh`、`quick-install.sh`)
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-06-05 — 安装向导 Docker Redis 检测修复
|
||||
|
||||
## 摘要
|
||||
|
||||
修复 Docker 部署下安装向导步骤 2 误报 `127.0.0.1:6379 Connection refused`。
|
||||
Compose 栈中 Redis 仅在容器内网 `redis:6379`,未映射宿主机端口。
|
||||
|
||||
## 变更
|
||||
|
||||
- `server/api/install.py`:`env-check` 读取 `NEXUS_REDIS_URL` / `REDIS_HOST`
|
||||
- 返回 `docker_defaults`(db_host=mysql, redis_host=redis 等)供步骤 3 预填
|
||||
- `web/app/install.html`:环境检测后自动应用 `docker_defaults`
|
||||
|
||||
## 验证
|
||||
|
||||
- Docker 容器内 `env-check` Redis 应对 `redis:6379` 显示 ✓
|
||||
- 步骤 3 数据库主机应为 `mysql`,Redis 为 `redis`
|
||||
|
||||
## 已部署容器热修
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/sync-install-wizard-to-container.sh # 同步 install.html + install.py 并重启
|
||||
```
|
||||
|
||||
无需完整 rebuild;若仍失败再 `bash deploy/nexus-1panel.sh upgrade`。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-06-05 — 安装脚本不再提示 Gitea 令牌
|
||||
|
||||
## 摘要
|
||||
|
||||
公共仓库已支持匿名 clone/pull,移除 `nexus-1panel.sh` 在未找到
|
||||
`nexus-1panel.secrets.sh` 时的 WARN 提示;文档与 example 标明令牌为可选项。
|
||||
|
||||
## 动机
|
||||
|
||||
用户新机 `curl | bash` 安装时看到「未找到 secrets 文件」误以为必须配置令牌。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `deploy/nexus-1panel.sh` — 删除 load_secrets 警告块
|
||||
- `deploy/install-nexus-fresh.sh` — 注释更新
|
||||
- `deploy/README-1panel.md` — 明确无需令牌
|
||||
- `deploy/nexus-1panel.secrets.sh.example` — 改为可选私人仓库说明
|
||||
|
||||
## 验证
|
||||
|
||||
- 无 secrets 文件时 `load_secrets` 静默,公共 URL clone 正常
|
||||
- 仍可通过 `init-token` 或 secrets 文件为私人仓库配置令牌
|
||||
@@ -0,0 +1,20 @@
|
||||
# 2026-06-05 — nx 符号链接路径修复
|
||||
|
||||
## 问题
|
||||
|
||||
`/usr/local/bin/nx` → `/opt/nexus/deploy/nx` 时,`dirname $BASH_SOURCE` 解析为
|
||||
`/usr/local/bin`,`source nexus-1panel.sh` 失败。
|
||||
|
||||
## 修复
|
||||
|
||||
- `deploy/nx`:使用 `readlink -f` 解析真实路径
|
||||
- `install-nexus-fresh.sh`:注册 `nexus-1panel` → `deploy/nexus-1panel.sh`
|
||||
|
||||
## 服务器立即修复
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
ln -sf /opt/nexus/deploy/nx /usr/local/bin/nx
|
||||
ln -sf /opt/nexus/deploy/nexus-1panel.sh /usr/local/bin/nexus-1panel
|
||||
nx god
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# 2026-06-06 — 生产镜像内置 install.html + 升级校验
|
||||
|
||||
## 摘要
|
||||
|
||||
修复 Docker 生产镜像缺少安装向导静态页导致 `/app/install.html` 404;构建时校验文件存在;升级脚本自动检测并可用 `sync-install-wizard-to-container.sh` 兜底。
|
||||
|
||||
## 动机
|
||||
|
||||
Vite 构建产物仅含 SPA `index.html`,`install.html` 需单独 COPY;此前依赖手工 `docker cp`,容器重建后丢失。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `Dockerfile.prod` — COPY install.html + vendor,`RUN test -f` 构建门控
|
||||
- `deploy/sync-install-wizard-to-container.sh`(新建)
|
||||
- `deploy/nexus-1panel.sh` — `verify_install_wizard`
|
||||
- `deploy/README-1panel.md` · `docker/README.md`
|
||||
- `deploy/install-nexus-fresh.sh` — chmod 同步脚本
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
已部署服务器需:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/nexus-1panel.sh upgrade
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8600/app/install.html # 200
|
||||
docker exec nexus-prod-nexus-1 test -f /app/web/app/install.html
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-06-06 — 安装脚本按机自动生成唯一密钥
|
||||
|
||||
## 摘要
|
||||
|
||||
Docker / 1Panel 全新安装路径统一通过 `scripts/generate_nexus_secrets.py` 生成每台服务器独立的 `MYSQL_*` 与 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`,写入 `docker/.env.prod` 并备份到 `/root/.nexus-install-secrets-*.env`。安装向导 `init-db` 复用容器环境内预生成密钥,避免与 Compose 不一致。
|
||||
|
||||
## 动机
|
||||
|
||||
新机安装不应从 `nexus-1panel.secrets.sh` 复制生产密钥;此前 `--fresh` 留空密钥导致 Compose 校验失败或各机密钥相同。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `scripts/generate_nexus_secrets.py`(新建)
|
||||
- `deploy/nexus-1panel.sh` — `write_env_prod` / `--reuse-secrets` / `--fresh` 忽略 secrets 中 NEXUS 项
|
||||
- `deploy/install-nexus-fresh.sh` / `deploy/quick-install.sh`
|
||||
- `docker/generate_env.py` — 复用同一生成器
|
||||
- `docker/entrypoint.sh` — `NEXUS_INSTALL_WIZARD_PENDING=1` 时不写 `/app/.env`
|
||||
- `docker/docker-compose.prod.yml` — 传递 `NEXUS_INSTALL_WIZARD_PENDING`
|
||||
- `server/api/install.py` — `_install_keys_from_environment()`
|
||||
- `deploy/README-1panel.md` · `deploy/nexus-1panel.secrets.sh.example`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
已部署实例无需变更。新机安装后需完成 `/app/install.html` 向导。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
python3 scripts/generate_nexus_secrets.py write-prod \
|
||||
--template docker/.env.prod.example --output /tmp/t.env.prod \
|
||||
--domain test.example.com --profile-env docker/profiles/2c8g.env --fresh --wizard-pending
|
||||
grep NEXUS_SECRET_KEY /tmp/t.env.prod
|
||||
ruff check server/api/install.py scripts/generate_nexus_secrets.py
|
||||
```
|
||||
@@ -119,7 +119,26 @@ sudo bash quick_start.sh
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — 准备代码与生产 env
|
||||
### Phase 2 — 一键安装(推荐)
|
||||
|
||||
在 1Panel **终端**:
|
||||
|
||||
```bash
|
||||
export NEXUS_GITEA_TOKEN='你的Gitea令牌'
|
||||
export NEXUS_DOMAIN='api.synaglobal.vip'
|
||||
# 从宝塔迁机:
|
||||
# export NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
|
||||
|
||||
cd /opt/nexus 2>/dev/null && git pull || true
|
||||
# 若尚未 clone,从任意目录:
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
# 或已 clone 后:
|
||||
# cd /opt/nexus && bash deploy/install-1panel-docker.sh --skip-clone
|
||||
```
|
||||
|
||||
脚本:`deploy/install-1panel-docker.sh`(clone → `.env.prod` → `compose up` → `/health`)。
|
||||
|
||||
### Phase 2b — 手工准备代码与生产 env(可选)
|
||||
|
||||
在 1Panel **终端**或 SSH:
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate per-host Nexus secrets (install / Docker).
|
||||
|
||||
Each fresh install must get unique MYSQL_* and NEXUS_* keys. Matches install wizard:
|
||||
SECRET_KEY = token_hex(32)
|
||||
API_KEY = token_hex(16)
|
||||
ENCRYPTION_KEY = urlsafe_b64encode(token_bytes(32))
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
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),
|
||||
"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(),
|
||||
}
|
||||
|
||||
|
||||
def parse_env_file(path: Path) -> dict[str, str]:
|
||||
data: dict[str, str] = {}
|
||||
if not path.is_file():
|
||||
return data
|
||||
for line in read_utf8_lf(path).splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
data[key.strip()] = value.strip().strip('"').strip("'")
|
||||
return data
|
||||
|
||||
|
||||
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()
|
||||
for key in (
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
"NEXUS_SECRET_KEY",
|
||||
"NEXUS_API_KEY",
|
||||
"NEXUS_ENCRYPTION_KEY",
|
||||
):
|
||||
val = raw.get(key, "").strip()
|
||||
if val:
|
||||
out[key] = val
|
||||
if not out.get("NEXUS_ENCRYPTION_KEY"):
|
||||
dburl = raw.get("NEXUS_DATABASE_URL", "")
|
||||
m = re.search(r"mysql\+aiomysql://[^:]+:([^@]+)@", dburl)
|
||||
if m and not out.get("MYSQL_PASSWORD"):
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
out["MYSQL_PASSWORD"] = unquote_plus(m.group(1))
|
||||
return out
|
||||
|
||||
|
||||
def apply_profile_pools(lines: list[str], profile_env: Path) -> list[str]:
|
||||
if not profile_env.is_file():
|
||||
return lines
|
||||
prof = parse_env_file(profile_env)
|
||||
pool = prof.get("NEXUS_DB_POOL_SIZE", "")
|
||||
overflow = prof.get("NEXUS_DB_MAX_OVERFLOW", "")
|
||||
out: list[str] = []
|
||||
for line in lines:
|
||||
if pool and line.startswith("NEXUS_DB_POOL_SIZE="):
|
||||
out.append(f"NEXUS_DB_POOL_SIZE={pool}")
|
||||
elif overflow and line.startswith("NEXUS_DB_MAX_OVERFLOW="):
|
||||
out.append(f"NEXUS_DB_MAX_OVERFLOW={overflow}")
|
||||
else:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def write_env_prod(
|
||||
template: Path,
|
||||
output: Path,
|
||||
*,
|
||||
domain: str,
|
||||
secrets_map: dict[str, str],
|
||||
profile_env: Path | None,
|
||||
wizard_pending: bool,
|
||||
) -> None:
|
||||
if not template.is_file():
|
||||
raise SystemExit(f"Missing template: {template}")
|
||||
|
||||
lines: list[str] = []
|
||||
for line in read_utf8_lf(template).splitlines():
|
||||
key = line.split("=", 1)[0].strip() if "=" in line and not line.strip().startswith("#") else ""
|
||||
if key in secrets_map:
|
||||
lines.append(f"{key}={secrets_map[key]}")
|
||||
elif key == "NEXUS_CORS_ORIGINS":
|
||||
if domain == "localhost":
|
||||
lines.append(line)
|
||||
else:
|
||||
lines.append(f"NEXUS_CORS_ORIGINS=https://{domain}")
|
||||
elif key == "NEXUS_API_BASE_URL":
|
||||
if domain == "localhost":
|
||||
lines.append(line)
|
||||
else:
|
||||
lines.append(f"NEXUS_API_BASE_URL=https://{domain}")
|
||||
else:
|
||||
lines.append(line)
|
||||
|
||||
if wizard_pending:
|
||||
found = False
|
||||
new_lines: list[str] = []
|
||||
for line in lines:
|
||||
if line.startswith("NEXUS_INSTALL_WIZARD_PENDING="):
|
||||
new_lines.append("NEXUS_INSTALL_WIZARD_PENDING=1")
|
||||
found = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
if not found:
|
||||
new_lines.append("")
|
||||
new_lines.append("# Set 0 after /app/install.html completes (init-db writes /app/.env)")
|
||||
new_lines.append("NEXUS_INSTALL_WIZARD_PENDING=1")
|
||||
lines = new_lines
|
||||
|
||||
if profile_env:
|
||||
lines = apply_profile_pools(lines, profile_env)
|
||||
|
||||
header = [
|
||||
f"# Generated {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
|
||||
"# Unique secrets for this host — do not copy to other servers",
|
||||
"",
|
||||
]
|
||||
text = "\n".join(header + lines) + "\n"
|
||||
write_utf8_lf(output, text)
|
||||
output.chmod(0o600)
|
||||
|
||||
|
||||
def write_secrets_backup(path: Path, secrets_map: dict[str, str], *, domain: str) -> None:
|
||||
lines = [
|
||||
f"# Nexus install secrets backup — {datetime.now(timezone.utc).isoformat()}",
|
||||
f"# Domain: {domain}",
|
||||
"# Store only on this server (chmod 600). Do NOT commit to Git.",
|
||||
"",
|
||||
]
|
||||
for key in (
|
||||
"MYSQL_ROOT_PASSWORD",
|
||||
"MYSQL_PASSWORD",
|
||||
"NEXUS_SECRET_KEY",
|
||||
"NEXUS_API_KEY",
|
||||
"NEXUS_ENCRYPTION_KEY",
|
||||
):
|
||||
if key in secrets_map:
|
||||
lines.append(f"{key}={secrets_map[key]}")
|
||||
write_utf8_lf(path, "\n".join(lines) + "\n")
|
||||
path.chmod(0o600)
|
||||
|
||||
|
||||
def cmd_shell(secrets_map: dict[str, str]) -> None:
|
||||
for key, val in secrets_map.items():
|
||||
print(f"export {key}={shlex.quote(val)}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Generate unique Nexus install secrets")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_shell = sub.add_parser("shell", help="Print export statements for bash eval")
|
||||
p_shell.add_argument("--from-env", type=Path, help="Import keys from existing .env")
|
||||
|
||||
p_write = sub.add_parser("write-prod", help="Write docker/.env.prod from template")
|
||||
p_write.add_argument("--template", type=Path, required=True)
|
||||
p_write.add_argument("--output", type=Path, required=True)
|
||||
p_write.add_argument("--domain", required=True)
|
||||
p_write.add_argument("--profile-env", type=Path, default=None)
|
||||
p_write.add_argument("--from-env", type=Path, default=None, help="Migration: import NEXUS keys")
|
||||
p_write.add_argument(
|
||||
"--fresh",
|
||||
action="store_true",
|
||||
help="New host: always generate new secrets (ignore process env)",
|
||||
)
|
||||
p_write.add_argument("--backup", type=Path, default=None, help="Write chmod 600 backup file")
|
||||
p_write.add_argument(
|
||||
"--wizard-pending",
|
||||
action="store_true",
|
||||
help="Set NEXUS_INSTALL_WIZARD_PENDING=1 in .env.prod",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cmd == "shell":
|
||||
if args.from_env:
|
||||
secrets_map = import_secrets_from_env(args.from_env)
|
||||
else:
|
||||
secrets_map = generate_nexus_secrets()
|
||||
cmd_shell(secrets_map)
|
||||
return
|
||||
|
||||
if args.cmd == "write-prod":
|
||||
if args.from_env and args.from_env.is_file():
|
||||
secrets_map = import_secrets_from_env(args.from_env)
|
||||
wizard = False
|
||||
elif args.fresh:
|
||||
secrets_map = generate_nexus_secrets()
|
||||
wizard = bool(args.wizard_pending)
|
||||
else:
|
||||
secrets_map = generate_nexus_secrets()
|
||||
wizard = bool(args.wizard_pending)
|
||||
|
||||
write_env_prod(
|
||||
args.template,
|
||||
args.output,
|
||||
domain=args.domain,
|
||||
secrets_map=secrets_map,
|
||||
profile_env=args.profile_env,
|
||||
wizard_pending=wizard,
|
||||
)
|
||||
if args.backup:
|
||||
write_secrets_backup(args.backup, secrets_map, domain=args.domain)
|
||||
print(f"Wrote {args.output}")
|
||||
if args.backup:
|
||||
print(f"Backup {args.backup}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+69
-12
@@ -12,7 +12,7 @@ import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote_plus
|
||||
from urllib.parse import quote_plus, urlparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -98,6 +98,47 @@ def _build_redis_url(req: InitDbRequest) -> str:
|
||||
return url
|
||||
|
||||
|
||||
def _resolve_redis_probe() -> tuple[str, int, str | None]:
|
||||
"""Redis target for install wizard health check (Docker uses redis:6379, not host loopback)."""
|
||||
url = os.environ.get("NEXUS_REDIS_URL", "").strip()
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
host = parsed.hostname or "127.0.0.1"
|
||||
port = parsed.port or 6379
|
||||
return host, port, parsed.password
|
||||
|
||||
host = os.environ.get("REDIS_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("REDIS_PORT", "6379"))
|
||||
return host, port, 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"):
|
||||
return None
|
||||
return {
|
||||
"db_host": os.environ.get("MYSQL_HOST", "mysql"),
|
||||
"db_port": os.environ.get("MYSQL_PORT", "3306"),
|
||||
"db_name": "nexus",
|
||||
"db_user": "nexus",
|
||||
"redis_host": "host.docker.internal",
|
||||
"redis_port": "6379",
|
||||
"redis_db": "0",
|
||||
}
|
||||
|
||||
|
||||
def _install_keys_from_environment() -> tuple[str, str, str] | None:
|
||||
"""Reuse keys pre-generated by Docker install (docker/.env.prod → compose env)."""
|
||||
import os
|
||||
|
||||
sk = os.environ.get("NEXUS_SECRET_KEY", "").strip()
|
||||
ak = os.environ.get("NEXUS_API_KEY", "").strip()
|
||||
ek = os.environ.get("NEXUS_ENCRYPTION_KEY", "").strip()
|
||||
if sk and ak and ek:
|
||||
return sk, ak, ek
|
||||
return None
|
||||
|
||||
|
||||
def _escape_env_value(value: str) -> str:
|
||||
"""Escape a value for safe inclusion in a .env file.
|
||||
|
||||
@@ -401,25 +442,32 @@ async def env_check():
|
||||
"pass": False,
|
||||
})
|
||||
|
||||
# Redis
|
||||
# Redis — 用户自行安装(1Panel/宿主机),不阻塞安装向导
|
||||
redis_ok = False
|
||||
redis_msg = "未安装"
|
||||
redis_msg = "未检测"
|
||||
rh, rp, rpass = _resolve_redis_probe()
|
||||
redis_label = f"{rh}:{rp}"
|
||||
try:
|
||||
import redis as rlib
|
||||
try:
|
||||
r = rlib.Redis(host="127.0.0.1", port=6379, socket_timeout=0.5)
|
||||
r = rlib.Redis(
|
||||
host=rh,
|
||||
port=rp,
|
||||
password=rpass or None,
|
||||
socket_timeout=0.5,
|
||||
)
|
||||
r.ping()
|
||||
redis_ok = True
|
||||
redis_msg = "✓ 已连接 127.0.0.1:6379"
|
||||
redis_msg = f"✓ 已连接 {redis_label}"
|
||||
except Exception as e:
|
||||
redis_msg = f"连接失败: {e}"
|
||||
redis_msg = f"未连接 {redis_label}(可继续,步骤3配置或稍后安装 Redis)— {e}"
|
||||
except ImportError:
|
||||
redis_msg = "redis 库未安装"
|
||||
redis_msg = "redis 库未安装(可继续)"
|
||||
checks.append({
|
||||
"name": "Redis",
|
||||
"required": "已连接",
|
||||
"required": "自行安装(步骤3配置)",
|
||||
"current": redis_msg,
|
||||
"pass": redis_ok,
|
||||
"pass": True,
|
||||
})
|
||||
|
||||
# MySQL database — user configures in step 3
|
||||
@@ -448,7 +496,11 @@ async def env_check():
|
||||
})
|
||||
|
||||
all_pass = all(c["pass"] for c in checks)
|
||||
return {"checks": checks, "all_pass": all_pass}
|
||||
payload: dict = {"checks": checks, "all_pass": all_pass}
|
||||
defaults = _docker_wizard_defaults()
|
||||
if defaults:
|
||||
payload["docker_defaults"] = defaults
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/init-db")
|
||||
@@ -512,11 +564,16 @@ async def init_db(req: InitDbRequest):
|
||||
except Exception:
|
||||
pool_size, max_overflow = 160, 120
|
||||
|
||||
# Generate keys
|
||||
# Keys: reuse Docker install pre-seed, else generate (must match install wizard)
|
||||
preseed = _install_keys_from_environment()
|
||||
if preseed:
|
||||
secret_key, api_key, encryption_key = preseed
|
||||
logger.info("Using pre-generated NEXUS keys from install environment")
|
||||
else:
|
||||
secret_key = secrets.token_hex(32)
|
||||
api_key = secrets.token_hex(16)
|
||||
# Generate Fernet-compatible encryption key (32 url-safe base64 bytes)
|
||||
import base64
|
||||
|
||||
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
|
||||
|
||||
# Insert settings
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
<!-- 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>
|
||||
@@ -639,6 +640,9 @@ function installWizard() {
|
||||
const d = await r.json();
|
||||
this.envChecks = d.checks;
|
||||
this.envAllPass = d.all_pass;
|
||||
if (d.docker_defaults) {
|
||||
Object.assign(this.form, d.docker_defaults);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
this.error = '环境检测请求失败: ' + e.message;
|
||||
|
||||
Reference in New Issue
Block a user