Compare commits

...

20 Commits

Author SHA1 Message Date
Nexus Deploy 536d520744 fix(nx): resolve deploy dir when invoked via /usr/local/bin symlink
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
readlink -f fixes sourcing nexus-1panel.sh; register nexus-1panel global cmd.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:49:34 +08:00
Nexus Deploy 6276ea08b7 feat(docker): remove bundled Redis; use external host install
Compose stack is MySQL+Nexus only; wizard Redis check is non-blocking
and defaults to host.docker.internal for self-managed Redis.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:48:00 +08:00
Nexus Deploy c81fe3183f fix(deploy): sync install.py into container for Redis env-check hotfix
sync-install-wizard-to-container.sh now copies server/api/install.py and
restarts Nexus so Docker installs pick up redis:6379 without full rebuild.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:34:53 +08:00
Nexus Deploy 5e7c5beb43 fix(install): probe redis via docker service host in env-check
Install wizard no longer hardcodes 127.0.0.1:6379; Docker installs get
mysql/redis prefills and a passing Redis check inside the nexus container.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:31:11 +08:00
Nexus Deploy 9b7dba99ff fix(install): drop Gitea token warning for public repo installs
Public anonymous clone needs no secrets file; document token as optional
for private repos only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:20:56 +08:00
Nexus Deploy 222703d733 fix(install): allow curl-pipe bash when set -u is enabled
BASH_SOURCE[0] is unset for stdin scripts; call main directly in
install-1panel-docker.sh and quick-install.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:11:40 +08:00
Nexus Deploy d884ea00ca feat(install): add full-stack install-1panel-docker.sh for fresh servers
Replace the nx redirect stub with a real orchestrator that chains 1Panel
setup, Docker Compose checks, and install-nexus-fresh; document UI-only
OpenResty proxy rules in README-1panel.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:00:24 +08:00
Nexus Deploy 422dcae0ff fix(docker): bake install.html into prod image with build verify and sync script 2026-06-06 01:14:59 +08:00
Nexus Deploy 343def7e77 fix(docker): include install.html and wizard vendor assets in prod image 2026-06-06 00:36:03 +08:00
Nexus Deploy 83ce11f55c feat(install): per-host auto-generated secrets for Docker installs
Each fresh 1Panel/Docker install generates unique MYSQL and NEXUS keys
via scripts/generate_nexus_secrets.py; install wizard reuses compose env.
2026-06-06 00:25:57 +08:00
Nexus Deploy e613aecc8e docs: clarify Gitea web URL vs git clone URL in README 2026-06-06 00:19:04 +08:00
Nexus Deploy f71da47ccf docs: add root README for Gitea product page and install scripts 2026-06-06 00:14:34 +08:00
Nexus Deploy 1acac6439a feat(deploy): public repo quick-install via curl (1Panel style)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 00:11:43 +08:00
Nexus Deploy feda614c52 fix(deploy): register nx/nexus-fresh in PATH for root
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:58:37 +08:00
Nexus Deploy 6cef3d2942 fix(deploy): private Gitea raw 404 — use clone or token API download
Anonymous curl saved "Not found." causing "Not: command not found";
add download-install-fresh.sh, --skip-clone, and script self-check.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:52:48 +08:00
Nexus Deploy 00f44032ff feat(deploy): add 1Panel-style fresh install script for empty DB
install-nexus-fresh.sh with phased checks, install wizard URL, and nx god
menu hints; wired into nx install-fresh and install menu option 6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:44:38 +08:00
Nexus Deploy 8ddcf2ad0d feat(deploy): add nx interactive installer and god ops menu
Single entry deploy/nx for install wizard, post-install restart/upgrade/logs,
and install-auto for non-interactive 2c8g setup on 1Panel Docker hosts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:41:04 +08:00
Nexus Deploy 65e4478b4b feat(deploy): load credentials from nexus-1panel.secrets.sh
Keep secrets out of tracked scripts; support git add -f secrets file,
git remote token parsing, and port/firewall checks for one-click install.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:37:37 +08:00
Nexus Deploy 2f830424a4 feat(deploy): unified 1Panel script with profiles and port checks
Merge install/upgrade into nexus-1panel.sh with 1c4g/2c8g/4c16g resource
profiles; preflight Gitea and firewall hints; post-install HTTPS health probe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 22:59:47 +08:00
Nexus Deploy 7b33b745a0 feat(deploy): add 1Panel Docker one-click install script
Automates clone, docker/.env.prod, compose up, and health check for 2C8G prod stack.
2026-06-05 22:52:44 +08:00
43 changed files with 3417 additions and 94 deletions
+2
View File
@@ -11,6 +11,8 @@ build/
.install_locked .install_locked
.install_state.json .install_state.json
SECRETS.md SECRETS.md
.git-askpass-nexus.sh
deploy/nexus-1panel.secrets.sh
*.pem *.pem
.env.* .env.*
.venv/ .venv/
+14 -1
View File
@@ -27,8 +27,21 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/ COPY server/ ./server/
COPY web/agent/ ./web/agent/ COPY web/agent/ ./web/agent/
COPY --from=frontend /build/web/app ./web/app/ 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 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 \ ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
+212
View File
@@ -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**。生产密钥仅保存在服务器与本机私密文档中。
+101
View File
@@ -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 Docker1Panel 已就绪)
```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 Docker1Panel 已装好) |
| `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
```
+56
View File
@@ -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
+53
View File
@@ -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
+10
View File
@@ -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":"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":"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-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"}
+275
View File
@@ -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 "未检测到 1Panel1pctl 不存在)"
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 "自动安装 Dockerget.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 + NexusRedis 请自行安装)..."
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 "$@"
+309
View File
@@ -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 Dockercurl 入口)"
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 "$@"
+14
View File
@@ -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'
+844
View File
@@ -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 "构建并启动(首次约 1020 分钟)..."
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
+391
View File
@@ -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核16G4c16g
[3] 安装 — 1核4G1c4g
[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 "重启 NexusPython..."
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] 重启 NexusPython 应用)
[2] 重启全部容器(mysql + redis + nexus
[3] 启动 Compose 栈
[4] 停止 Compose 栈
[5] 重建 Nexus 镜像并启动(up -d --build nexus
── 观测 ──
[6] 状态 + 健康检查
[7] 查看 Nexus 日志(跟踪)
[8] 查看 MySQL / Redis 日志
[9] 端口 / 防火墙检查
── 发布 ──
[a] 检查 Git 更新
[b] 一键升级(备份 + pull + 重建)
[c] 立即备份 MySQL
── 其他 ──
[d] 进入 Nexus 容器 shell
[e] 打开 1Panel 反代说明
[0] 返回主菜单
EOF
read -r -p "请选择: " choice
case "$choice" in
1) god_restart_nexus; pause_enter ;;
2) god_restart_stack; pause_enter ;;
3) god_start_stack; pause_enter ;;
4) god_stop_stack; pause_enter ;;
5)
compose_cmd "$NEXUS_ROOT" up -d --build nexus
verify_health "$NEXUS_ROOT" "重建" || true
pause_enter
;;
6) god_status; pause_enter ;;
7) god_logs nexus ;;
8)
echo "1) mysql 2) redis"
read -r -p "选: " s
[[ "$s" == "1" ]] && god_logs mysql || god_logs redis
;;
9)
check_ports_preflight "$NEXUS_ROOT"
check_ports_post "$NEXUS_ROOT"
pause_enter
;;
a|A)
cmd_check
pause_enter
;;
b|B) god_upgrade_interactive ;;
c|C) god_backup_now; pause_enter ;;
d)
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
;;
e)
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
info "示例配置: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
pause_enter
;;
0) return 0 ;;
*) warn "无效选项"; sleep 1 ;;
esac
done
}
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 "$@"
+124
View File
@@ -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.pyDocker 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 "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# 兼容入口 — 请优先使用 deploy/nexus-1panel.sh
exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/nexus-1panel.sh" upgrade "$@"
+10 -2
View File
@@ -1,14 +1,18 @@
# Production Compose env — copy to docker/.env.prod (never commit) # 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_ROOT_PASSWORD=change_me_root_strong
MYSQL_PASSWORD=change_me_nexus_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_SECRET_KEY=
NEXUS_API_KEY= NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY= NEXUS_ENCRYPTION_KEY=
# 1 = entrypoint must not write /app/.env until install wizard step 3
NEXUS_INSTALL_WIZARD_PENDING=0
# Public URL # Public URL
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
NEXUS_API_BASE_URL=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_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20 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 # Optional
# NEXUS_PUBLISH_PORT=8600 # NEXUS_PUBLISH_PORT=8600
+5 -3
View File
@@ -44,8 +44,8 @@ docker compose build --no-cache nexus
| 服务 | 端口 | 说明 | | 服务 | 端口 | 说明 |
|------|------|------| |------|------|------|
| nexus | 8600 | FastAPI + 静态 `/app/` | | nexus | 8600 | FastAPI + 静态 `/app/` |
| mysql | 3306 | 数据库 `nexus` | | mysql | 3306 | 数据库 `nexus`Compose 内) |
| redis | 6379 | 仅容器内网(未映射到主机) | | 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 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` - 仅绑定 `127.0.0.1:8600`OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
- **2C8G 调优**`docker/mysql-prod-2c8g.cnf`buffer pool 1G、`max_connections=120`+ 容器 `mem_limit` + `NEXUS_DB_POOL_SIZE=30` - **2C8G 调优**`docker/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` - 完整迁移步骤:`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` 详见 `docs/design/specs/2026-06-03-docker-design.md`
+16 -38
View File
@@ -1,9 +1,11 @@
# Nexus 6.0 — production Compose (1Panel / bare Docker host) # 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: # Usage:
# cp docker/.env.prod.example docker/.env.prod # fill secrets from Baota .env # bash deploy/nexus-1panel.sh install --profile 2c8g
# docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up -d --build # # 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 # 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} MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
volumes: volumes:
- mysql-data:/var/lib/mysql - mysql-data:/var/lib/mysql
- ./mysql-prod-2c8g.cnf:/etc/mysql/conf.d/99-nexus-2c8g.cnf:ro - ./${MYSQL_CNF_FILE:-mysql-prod-2c8g.cnf}:/etc/mysql/conf.d/99-nexus-prod.cnf:ro
mem_limit: 2g mem_limit: ${MYSQL_MEM_LIMIT:-2g}
cpus: 1.5 cpus: ${MYSQL_CPUS:-1.5}
healthcheck: healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"] test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
interval: 10s interval: 10s
@@ -32,29 +34,6 @@ services:
networks: networks:
- nexus-internal - 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: nexus:
build: build:
context: .. context: ..
@@ -63,34 +42,34 @@ services:
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
redis: extra_hosts:
condition: service_healthy - "host.docker.internal:host-gateway"
ports: ports:
- "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600" - "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600"
environment: environment:
MYSQL_HOST: mysql MYSQL_HOST: mysql
MYSQL_PORT: "3306" MYSQL_PORT: "3306"
REDIS_HOST: redis
REDIS_PORT: "6379"
NEXUS_PERSIST_DIR: /var/lib/nexus NEXUS_PERSIST_DIR: /var/lib/nexus
NEXUS_DOCKER_WRITE_ENV: "1" NEXUS_DOCKER_WRITE_ENV: "1"
NEXUS_HOST: "0.0.0.0" NEXUS_HOST: "0.0.0.0"
NEXUS_PORT: "8600" NEXUS_PORT: "8600"
NEXUS_DEPLOY_PATH: /app NEXUS_DEPLOY_PATH: /app
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD}@mysql:3306/nexus 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_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY} NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_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_API_BASE_URL: ${NEXUS_API_BASE_URL:?set NEXUS_API_BASE_URL}
NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30} NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30}
NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20} NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20}
volumes: volumes:
- nexus-state:/var/lib/nexus - nexus-state:/var/lib/nexus
- nexus-web-data:/app/web/data - nexus-web-data:/app/web/data
mem_limit: 2g mem_limit: ${NEXUS_MEM_LIMIT:-2g}
cpus: 1.5 cpus: ${NEXUS_CPUS:-1.5}
healthcheck: healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"] test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
interval: 30s interval: 30s
@@ -106,6 +85,5 @@ networks:
volumes: volumes:
mysql-data: mysql-data:
redis-data:
nexus-state: nexus-state:
nexus-web-data: nexus-web-data:
+35 -3
View File
@@ -20,6 +20,30 @@ s.close()
done 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() { restore_persisted_env() {
if [ -f "${PERSIST_ENV}" ]; then if [ -f "${PERSIST_ENV}" ]; then
cp "${PERSIST_ENV}" /app/.env cp "${PERSIST_ENV}" /app/.env
@@ -38,6 +62,10 @@ write_env_from_environment() {
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
return 0 return 0
fi fi
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-}" = "1" ]; then
echo "entrypoint: install wizard pending — skip writing /app/.env"
return 0
fi
mkdir -p "${PERSIST_DIR}" mkdir -p "${PERSIST_DIR}"
{ {
echo "# Generated by docker/entrypoint.sh — do not commit" 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_HOST="${MYSQL_HOST:-mysql}"
MYSQL_PORT="${MYSQL_PORT:-3306}" MYSQL_PORT="${MYSQL_PORT:-3306}"
REDIS_HOST="${REDIS_HOST:-redis}"
REDIS_PORT="${REDIS_PORT:-6379}"
wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}" 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 restore_persisted_env
write_env_from_environment write_env_from_environment
+18 -30
View File
@@ -3,47 +3,35 @@
from __future__ import annotations from __future__ import annotations
import base64 import sys
import secrets
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent 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" OUT = ROOT / "docker" / ".env"
EXAMPLE = ROOT / "docker" / ".env.example" EXAMPLE = ROOT / "docker" / ".env.example"
def _token(n: int = 32) -> str:
return secrets.token_urlsafe(n)
def main() -> None: def main() -> None:
if OUT.exists(): if OUT.exists():
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.") raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
mysql_root = _token(24) secrets_map = generate_nexus_secrets()
mysql_pass = _token(24) write_env_prod(
secret = _token(32) EXAMPLE,
api_key = _token(24) OUT,
# Fernet-compatible (same as install wizard) domain="localhost",
enc_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode() secrets_map=secrets_map,
profile_env=None,
text = EXAMPLE.read_text(encoding="utf-8") wizard_pending=False,
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")
print(f"Wrote {OUT}") print(f"Wrote {OUT}")
print("Next: docker compose up -d --build") print("Next: docker compose up -d --build")
+20
View File
@@ -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
+20
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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`
+32
View File
@@ -0,0 +1,32 @@
# Changelog: NX 一键菜单 + 上帝运维面板
**日期**: 2026-06-04
## 摘要
新增 `deploy/nx` 交互脚本:安装向导(档位/迁机/端口检查)与安装后「上帝菜单」,可重启 Nexus Python 容器、重启全栈、查看日志、升级、MySQL 备份等。
## 动机
用户要求单一入口,安装与日常运维(重启 Python 等)全部在菜单内完成。
## 涉及文件
- `deploy/nx` — 主入口(菜单)
- `deploy/nexus-1panel.sh` — 可被 sourceCLI 子命令保留
- `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
```
## 安装向导
- DockerRedis 主机 `host.docker.internal`,端口 `6379`
- 步骤 2 Redis 未连接也可继续
@@ -0,0 +1,24 @@
# 2026-06-05 — Gitea 仓库产品说明 README
## 摘要
在仓库根目录新增 `README.md`,作为 Gitea 项目页产品说明:项目简介、公共仓库一键安装、NX 上帝菜单、deploy/docker/scripts 脚本索引、迁机与全新安装对照、文档链接。
## 动机
Gitea 仓库页原先无根 READMEDescription 为空),运维无法在仓库首页看到 `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 在 1Panel **终端**或 SSH
+237
View File
@@ -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()
+73 -16
View File
@@ -12,7 +12,7 @@ import logging
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
from urllib.parse import quote_plus from urllib.parse import quote_plus, urlparse
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
@@ -98,6 +98,47 @@ def _build_redis_url(req: InitDbRequest) -> str:
return url 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: def _escape_env_value(value: str) -> str:
"""Escape a value for safe inclusion in a .env file. """Escape a value for safe inclusion in a .env file.
@@ -401,25 +442,32 @@ async def env_check():
"pass": False, "pass": False,
}) })
# Redis # Redis — 用户自行安装(1Panel/宿主机),不阻塞安装向导
redis_ok = False redis_ok = False
redis_msg = "安装" redis_msg = "检测"
rh, rp, rpass = _resolve_redis_probe()
redis_label = f"{rh}:{rp}"
try: try:
import redis as rlib import redis as rlib
try: 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() r.ping()
redis_ok = True redis_ok = True
redis_msg = "✓ 已连接 127.0.0.1:6379" redis_msg = f"✓ 已连接 {redis_label}"
except Exception as e: except Exception as e:
redis_msg = f"连接失败: {e}" redis_msg = f"连接 {redis_label}(可继续,步骤3配置或稍后安装 Redis)— {e}"
except ImportError: except ImportError:
redis_msg = "redis 库未安装" redis_msg = "redis 库未安装(可继续)"
checks.append({ checks.append({
"name": "Redis", "name": "Redis",
"required": "已连接", "required": "自行安装(步骤3配置)",
"current": redis_msg, "current": redis_msg,
"pass": redis_ok, "pass": True,
}) })
# MySQL database — user configures in step 3 # MySQL database — user configures in step 3
@@ -448,7 +496,11 @@ async def env_check():
}) })
all_pass = all(c["pass"] for c in checks) 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") @router.post("/init-db")
@@ -512,12 +564,17 @@ async def init_db(req: InitDbRequest):
except Exception: except Exception:
pool_size, max_overflow = 160, 120 pool_size, max_overflow = 160, 120
# Generate keys # Keys: reuse Docker install pre-seed, else generate (must match install wizard)
secret_key = secrets.token_hex(32) preseed = _install_keys_from_environment()
api_key = secrets.token_hex(16) if preseed:
# Generate Fernet-compatible encryption key (32 url-safe base64 bytes) secret_key, api_key, encryption_key = preseed
import base64 logger.info("Using pre-generated NEXUS keys from install environment")
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode() else:
secret_key = secrets.token_hex(32)
api_key = secrets.token_hex(16)
import base64
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
# Insert settings # Insert settings
settings_kv = { settings_kv = {
+4
View File
@@ -182,6 +182,7 @@
<!-- Redis --> <!-- Redis -->
<div class="border-t-2 border-slate-100 pt-4 mt-4"> <div class="border-t-2 border-slate-100 pt-4 mt-4">
<h3 class="font-bold text-slate-700 mb-3">⚡ Redis 配置</h3> <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 class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 主机</label> <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(); const d = await r.json();
this.envChecks = d.checks; this.envChecks = d.checks;
this.envAllPass = d.all_pass; this.envAllPass = d.all_pass;
if (d.docker_defaults) {
Object.assign(this.form, d.docker_defaults);
}
} }
} catch(e) { } catch(e) {
this.error = '环境检测请求失败: ' + e.message; this.error = '环境检测请求失败: ' + e.message;