diff --git a/deploy/README-1panel.md b/deploy/README-1panel.md index 7c80a327..06a1785e 100644 --- a/deploy/README-1panel.md +++ b/deploy/README-1panel.md @@ -28,6 +28,12 @@ curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-in 安装完成后浏览器打开 `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。 diff --git a/deploy/deploy-on-server.sh b/deploy/deploy-on-server.sh new file mode 100644 index 00000000..d475026c --- /dev/null +++ b/deploy/deploy-on-server.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Run ON production server (宝塔终端 / ssh root@47.254.123.106) +# Pulls latest main from Gitea and restarts Nexus. Frontend tar must be uploaded separately +# if you did not build on this machine. +# +# Usage (on server): +# cd /www/wwwroot/api.synaglobal.vip +# bash deploy/deploy-on-server.sh +# +# Optional env: +# NEXUS_GITEA_TOKEN — if git pull needs auth +# SKIP_FRONTEND=1 — backend only + +set -euo pipefail + +DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/www/wwwroot/api.synaglobal.vip}" +cd "${DEPLOY_PATH}" + +echo "═══ Nexus server-side deploy ═══" +echo "Path: ${DEPLOY_PATH}" + +if [ -n "${NEXUS_GITEA_TOKEN:-}" ]; then + git remote set-url origin "http://admin:${NEXUS_GITEA_TOKEN}@66.154.115.8:3000/admin/Nexus.git" +fi + +echo "▶ git fetch + reset --hard origin/main..." +git fetch --all +git reset --hard origin/main +echo " HEAD: $(git log -1 --oneline)" + +echo "▶ supervisorctl restart nexus..." +supervisorctl restart nexus + +if [ -f /tmp/nexus-frontend.tar.gz ] && [ "${SKIP_FRONTEND:-0}" != "1" ]; then + echo "▶ extract /tmp/nexus-frontend.tar.gz..." + tar xzf /tmp/nexus-frontend.tar.gz -C web/app + rm -f /tmp/nexus-frontend.tar.gz + if [ -f deploy/prune_frontend_assets.py ]; then + python3 deploy/prune_frontend_assets.py --dry-run + python3 deploy/prune_frontend_assets.py + fi + supervisorctl restart nexus +fi + +sleep 3 +HEALTH=$(curl -s http://127.0.0.1:8600/health || true) +SPA=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/ || echo "000") +echo " /health → ${HEALTH}" +echo " /app/ → ${SPA}" + +if [ "${HEALTH}" = "ok" ] && [ "${SPA}" = "200" ]; then + echo "✓ Server deploy OK" +else + echo "✗ Verify failed — check: supervisorctl status nexus" >&2 + exit 1 +fi diff --git a/deploy/gate_log.jsonl b/deploy/gate_log.jsonl index 0353402f..fd41c8a1 100644 --- a/deploy/gate_log.jsonl +++ b/deploy/gate_log.jsonl @@ -156,3 +156,13 @@ {"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} {"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"text_eol","result":"PASS","detail":"git index LF"} {"ts":"2026-06-04T19:58:01+08:00","date":"2026-06-04","gate":"summary","result":"BLOCK","detail":"5/7"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"changelog","result":"PASS","detail":"2026-06-04-audit-phase2-p2-6-sync-engine-full.md 32lines"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"audit","result":"PASS","detail":"2026-06-04-docs-consolidation.md"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"test","result":"BLOCK","detail":"tests failed"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"lint","result":"PASS","detail":"0 violations"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"import","result":"PASS","detail":"ok"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"security","result":"PASS","detail":"0 HIGH"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"review","result":"BLOCK","detail":"audit missing changed files"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"text_eol","result":"PASS","detail":"git index LF"} +{"ts":"2026-06-04T23:47:38+08:00","date":"2026-06-04","gate":"summary","result":"BLOCK","detail":"5/7"} diff --git a/deploy/install-1panel-docker.sh b/deploy/install-1panel-docker.sh index 770b8093..95f63652 100644 --- a/deploy/install-1panel-docker.sh +++ b/deploy/install-1panel-docker.sh @@ -15,7 +15,7 @@ # 流程: # ① 可选:官方 1Panel 安装(含 Docker,交互式) # ② 检测 / 安装 Docker Compose -# ③ install-nexus-fresh.sh → MySQL + Redis + Nexus 容器 +# ③ install-nexus-fresh.sh → MySQL + Nexus 容器(Redis 自行安装) # # 反代:必须在 1Panel 面板 UI 配置,禁止脚本直接改 /opt/1panel/apps/ # 示例: deploy/1panel/openresty-nexus.conf.example @@ -245,7 +245,7 @@ download_fresh_installer() { } phase_nexus() { - step "安装 Nexus Docker 栈(MySQL + Redis + Nexus)..." + step "安装 Nexus Docker 栈(MySQL + Nexus;Redis 请自行安装)..." local installer if [[ -x "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" ]]; then installer="${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" diff --git a/deploy/nexus-1panel.sh b/deploy/nexus-1panel.sh index 02d416e6..c2ece4da 100644 --- a/deploy/nexus-1panel.sh +++ b/deploy/nexus-1panel.sh @@ -542,7 +542,7 @@ write_env_prod() { compose_up() { local root="$1" save_host_profile "$root" - info "档位: ${NEXUS_PROFILE}(MySQL/Redis/Nexus 内存与连接池已套用)" + info "档位: ${NEXUS_PROFILE}(MySQL/Nexus 内存与连接池已套用;Redis 请自行安装)" info "构建并启动(首次约 10~20 分钟)..." compose_cmd "$root" up -d --build } diff --git a/docker/.env.prod.example b/docker/.env.prod.example index 1831ef28..57d186c6 100644 --- a/docker/.env.prod.example +++ b/docker/.env.prod.example @@ -21,5 +21,9 @@ NEXUS_API_BASE_URL=https://api.synaglobal.vip NEXUS_DB_POOL_SIZE=30 NEXUS_DB_MAX_OVERFLOW=20 +# Redis 不在 Docker 栈内 — 请在宿主机或 1Panel 应用商店自行安装 +# Nexus 容器通过 host.docker.internal 访问宿主机 Redis(见 docker-compose.prod.yml) +NEXUS_REDIS_URL=redis://host.docker.internal:6379/0 + # Optional # NEXUS_PUBLISH_PORT=8600 diff --git a/docker/README.md b/docker/README.md index afb5832e..d4b00cf7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -44,8 +44,8 @@ docker compose build --no-cache nexus | 服务 | 端口 | 说明 | |------|------|------| | nexus | 8600 | FastAPI + 静态 `/app/` | -| mysql | 3306 | 数据库 `nexus` | -| redis | 6379 | 仅容器内网(未映射到主机) | +| mysql | 3306 | 数据库 `nexus`(Compose 内) | +| redis | — | **不在 Compose 内**;宿主机/1Panel 自建,默认 `host.docker.internal:6379` | ## 生产注意 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index adc6b9df..4acecdbf 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -34,29 +34,6 @@ services: networks: - nexus-internal - redis: - image: redis:7-alpine - restart: unless-stopped - command: - - redis-server - - --appendonly - - "yes" - - --maxmemory - - ${REDIS_MAXMEMORY:-256mb} - - --maxmemory-policy - - allkeys-lru - volumes: - - redis-data:/data - mem_limit: ${REDIS_MEM_LIMIT:-384m} - cpus: ${REDIS_CPUS:-0.5} - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 8 - networks: - - nexus-internal - nexus: build: context: .. @@ -65,22 +42,21 @@ services: depends_on: mysql: condition: service_healthy - redis: - condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" ports: - "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600" environment: MYSQL_HOST: mysql MYSQL_PORT: "3306" - REDIS_HOST: redis - REDIS_PORT: "6379" NEXUS_PERSIST_DIR: /var/lib/nexus NEXUS_DOCKER_WRITE_ENV: "1" NEXUS_HOST: "0.0.0.0" NEXUS_PORT: "8600" NEXUS_DEPLOY_PATH: /app NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD}@mysql:3306/nexus - NEXUS_REDIS_URL: redis://redis:6379/0 + # Redis 不在 Compose 内 — 请在宿主机/1Panel 自建,默认连宿主机 6379 + NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://host.docker.internal:6379/0} NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS} NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY} NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY} @@ -109,6 +85,5 @@ networks: volumes: mysql-data: - redis-data: nexus-state: nexus-web-data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 07a1cb30..b50867c1 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -20,6 +20,30 @@ s.close() done } +wait_tcp_optional() { + host="$1" + port="$2" + tries="${3:-15}" + echo "entrypoint: waiting for external Redis ${host}:${port} (max ${tries} tries)..." + n=0 + while [ "$n" -lt "$tries" ]; do + if python3 -c " +import socket +s = socket.socket() +s.settimeout(2) +s.connect(('${host}', ${port})) +s.close() +" 2>/dev/null; then + echo "entrypoint: Redis ready at ${host}:${port}" + return 0 + fi + n=$((n + 1)) + sleep 2 + done + echo "entrypoint: warn — Redis not reachable at ${host}:${port}; install on host or set NEXUS_REDIS_URL" + return 0 +} + restore_persisted_env() { if [ -f "${PERSIST_ENV}" ]; then cp "${PERSIST_ENV}" /app/.env @@ -64,11 +88,15 @@ trap persist_env_on_exit EXIT INT TERM MYSQL_HOST="${MYSQL_HOST:-mysql}" MYSQL_PORT="${MYSQL_PORT:-3306}" -REDIS_HOST="${REDIS_HOST:-redis}" -REDIS_PORT="${REDIS_PORT:-6379}" wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}" -wait_tcp "${REDIS_HOST}" "${REDIS_PORT}" + +# Redis 由用户自行安装在宿主机/1Panel;安装向导阶段不阻塞启动 +if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then + REDIS_HOST="${REDIS_HOST:-host.docker.internal}" + REDIS_PORT="${REDIS_PORT:-6379}" + wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15 +fi restore_persisted_env write_env_from_environment diff --git a/docker/profiles/1c4g.env b/docker/profiles/1c4g.env index bf1bd619..29cb6e13 100644 --- a/docker/profiles/1c4g.env +++ b/docker/profiles/1c4g.env @@ -2,9 +2,6 @@ MYSQL_CNF_FILE=mysql-prod-1c4g.cnf MYSQL_MEM_LIMIT=1g MYSQL_CPUS=1 -REDIS_MAXMEMORY=128mb -REDIS_MEM_LIMIT=256m -REDIS_CPUS=0.25 NEXUS_MEM_LIMIT=1g NEXUS_CPUS=1 NEXUS_DB_POOL_SIZE=15 diff --git a/docker/profiles/2c8g.env b/docker/profiles/2c8g.env index bc5c34a9..366587b7 100644 --- a/docker/profiles/2c8g.env +++ b/docker/profiles/2c8g.env @@ -2,9 +2,6 @@ MYSQL_CNF_FILE=mysql-prod-2c8g.cnf MYSQL_MEM_LIMIT=2g MYSQL_CPUS=1.5 -REDIS_MAXMEMORY=256mb -REDIS_MEM_LIMIT=384m -REDIS_CPUS=0.5 NEXUS_MEM_LIMIT=2g NEXUS_CPUS=1.5 NEXUS_DB_POOL_SIZE=30 diff --git a/docker/profiles/4c16g.env b/docker/profiles/4c16g.env index f2090879..1706c44f 100644 --- a/docker/profiles/4c16g.env +++ b/docker/profiles/4c16g.env @@ -2,9 +2,6 @@ MYSQL_CNF_FILE=mysql-prod-4c16g.cnf MYSQL_MEM_LIMIT=6g MYSQL_CPUS=3 -REDIS_MAXMEMORY=512mb -REDIS_MEM_LIMIT=768m -REDIS_CPUS=1 NEXUS_MEM_LIMIT=4g NEXUS_CPUS=3 NEXUS_DB_POOL_SIZE=60 diff --git a/docs/changelog/2026-06-05-external-redis-no-compose.md b/docs/changelog/2026-06-05-external-redis-no-compose.md new file mode 100644 index 00000000..4c6326f7 --- /dev/null +++ b/docs/changelog/2026-06-05-external-redis-no-compose.md @@ -0,0 +1,31 @@ +# 2026-06-05 — Docker 栈移除内置 Redis + +## 摘要 + +按用户要求,一键安装脚本 / `docker-compose.prod.yml` **不再附带 Redis 容器**。 +Redis 由用户在宿主机或 1Panel 应用商店自行安装;Nexus 默认连 `host.docker.internal:6379`。 + +## 变更 + +- `docker/docker-compose.prod.yml` — 删除 `redis` 服务与 `redis-data` 卷 +- `docker/entrypoint.sh` — 安装向导阶段不等待 Redis;否则有限次等待外置 Redis +- `server/api/install.py` — Redis 环境检测改为**不阻塞**(`pass: true`) +- `deploy/README-1panel.md` — 补充 Redis 自建说明 +- `docker/.env.prod.example` — 增加 `NEXUS_REDIS_URL` + +## 迁移(已装旧栈) + +```bash +cd /opt/nexus && git pull +docker stop nexus-prod-redis-1 2>/dev/null; docker rm nexus-prod-redis-1 2>/dev/null +# 在宿主机/1Panel 安装 Redis 并监听 6379 +docker compose -f docker/docker-compose.prod.yml \ + --env-file docker/.env.prod \ + --env-file docker/profiles/$(cat docker/.host-profile).env \ + up -d --build +``` + +## 安装向导 + +- Docker:Redis 主机 `host.docker.internal`,端口 `6379` +- 步骤 2 Redis 未连接也可继续 diff --git a/server/api/install.py b/server/api/install.py index 31f0fc8c..43fb3978 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -116,14 +116,13 @@ 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 - rh, rp, _ = _resolve_redis_probe() 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": rh, - "redis_port": str(rp), + "redis_host": "host.docker.internal", + "redis_port": "6379", "redis_db": "0", } @@ -443,9 +442,9 @@ async def env_check(): "pass": False, }) - # Redis — Docker Compose: redis:6379 (not published on host 127.0.0.1) + # Redis — 用户自行安装(1Panel/宿主机),不阻塞安装向导 redis_ok = False - redis_msg = "未安装" + redis_msg = "未检测" rh, rp, rpass = _resolve_redis_probe() redis_label = f"{rh}:{rp}" try: @@ -461,14 +460,14 @@ async def env_check(): redis_ok = True redis_msg = f"✓ 已连接 {redis_label}" except Exception as e: - redis_msg = f"连接失败: {e}" + redis_msg = f"未连接 {redis_label}(可继续,步骤3配置或稍后安装 Redis)— {e}" except ImportError: - redis_msg = "redis 库未安装" + redis_msg = "redis 库未安装(可继续)" checks.append({ "name": "Redis", - "required": "已连接", + "required": "自行安装(步骤3配置)", "current": redis_msg, - "pass": redis_ok, + "pass": True, }) # MySQL database — user configures in step 3 diff --git a/web/app/install.html b/web/app/install.html index 6d388933..8439f786 100644 --- a/web/app/install.html +++ b/web/app/install.html @@ -182,6 +182,7 @@

⚡ Redis 配置

+

请先在宿主机或 1Panel 安装 Redis。Docker 部署填主机 host.docker.internal,端口 6379。