diff --git a/deploy/README-1panel.md b/deploy/README-1panel.md index 93ab75a6..d4050222 100644 --- a/deploy/README-1panel.md +++ b/deploy/README-1panel.md @@ -110,8 +110,8 @@ Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安 `nx update` / `nexus-1panel.sh` 会: 1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`) -2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod` -3. 安装向导步骤 3 **预填容器名** +2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`) +3. 安装向导步骤 3 **预填 MySQL/Redis 容器名** ### 安装步骤 @@ -120,7 +120,7 @@ Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安 3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`) 4. 填写 MySQL 密码;Redis 若设了密码一并填写 -若仍报 `Can't connect to MySQL server on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行: +若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行: ```bash cd /opt/nexus && bash deploy/detect-1panel-services.sh diff --git a/deploy/nexus-1panel.sh b/deploy/nexus-1panel.sh index a7de5fc9..5050ee6a 100755 --- a/deploy/nexus-1panel.sh +++ b/deploy/nexus-1panel.sh @@ -226,7 +226,8 @@ sync_1panel_service_hosts() { NEXUS_1PANEL_REDIS_HOST=*) redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}" upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host" - info "1Panel Redis 容器: $redis_host" + upsert_env_var "$env_file" "NEXUS_REDIS_URL" "redis://${redis_host}:6379/0" + info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 已同步)" ;; esac done < <("$detect" 2>/dev/null || true) diff --git a/docker/README.md b/docker/README.md index d8328f84..ee1d111a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,7 +46,7 @@ docker compose build --no-cache nexus | 本地开发 `docker-compose.yml` | nexus + mysql + redis | 一键本地联调 | | 生产 `docker-compose.prod.yml` | **仅 nexus** | MySQL/Redis 宿主机或 1Panel 自建 | -生产默认 `NEXUS_REDIS_URL=redis://host.docker.internal:6379/0`;MySQL 由安装向导写入 `NEXUS_DATABASE_URL`。 +生产 1Panel:`nx update` 自动写入 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`;MySQL 由安装向导写入 `NEXUS_DATABASE_URL`。 ## 生产注意 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 8b907c3e..bace0537 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -26,7 +26,8 @@ services: NEXUS_HOST: "0.0.0.0" NEXUS_PORT: "8600" NEXUS_DEPLOY_PATH: /app - NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://host.docker.internal:6379/0} + # 1Panel: nx update 写入 redis://1Panel-redis-xxxx:6379/0;非 1Panel 回退 host.docker.internal + NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://${NEXUS_1PANEL_REDIS_HOST:-host.docker.internal}:6379/0} NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-} NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-} NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS} diff --git a/docs/changelog/2026-06-05-1panel-redis-container-url.md b/docs/changelog/2026-06-05-1panel-redis-container-url.md new file mode 100644 index 00000000..222326db --- /dev/null +++ b/docs/changelog/2026-06-05-1panel-redis-container-url.md @@ -0,0 +1,20 @@ +# 2026-06-05 — 1Panel Redis 容器名与 NEXUS_REDIS_URL 同步 + +## 摘要 + +与 MySQL 对齐:探测到 `1Panel-redis-*` 时同步更新 `NEXUS_REDIS_URL`;安装向导步骤 3 增加 Redis TCP 预检与 1panel-network 错误文案;Compose 默认 Redis URL 优先使用 `NEXUS_1PANEL_REDIS_HOST`。 + +## 涉及文件 + +- `deploy/nexus-1panel.sh` — sync 写入 `NEXUS_REDIS_URL` +- `server/api/install.py` — Redis 预检与 `_redis_connect_error_detail` +- `docker/docker-compose.prod.yml` — Redis URL 默认表达式 + +## 验证 + +```bash +bash deploy/detect-1panel-services.sh # 应输出 NEXUS_1PANEL_REDIS_HOST=... +grep NEXUS_REDIS_URL docker/.env.prod # redis://1Panel-redis-xxxx:6379/0 +nx update --no-cache +# 安装向导步骤 3 redis_host 预填容器名;步骤 4 Redis 连接通过 +``` diff --git a/server/api/install.py b/server/api/install.py index e91a2f64..ae57eb62 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -98,8 +98,18 @@ def _build_redis_url(req: InitDbRequest) -> str: return url +def _onepanel_service_host(kind: str) -> str | None: + """1Panel App Store container name (NEXUS_1PANEL_DB_HOST / REDIS_HOST).""" + val = os.environ.get(f"NEXUS_1PANEL_{kind}_HOST", "").strip() + return val or None + + def _resolve_redis_probe() -> tuple[str, int, str | None]: - """Redis target for install wizard (Docker: host.docker.internal → 宿主机自建 Redis).""" + """Redis target for install wizard (1Panel: container name on 1panel-network).""" + onepanel = _onepanel_service_host("REDIS") + if onepanel: + return onepanel, 6379, None + url = os.environ.get("NEXUS_REDIS_URL", "").strip() if url: parsed = urlparse(url) @@ -112,12 +122,6 @@ def _resolve_redis_probe() -> tuple[str, int, str | None]: return host, port, None -def _onepanel_service_host(kind: str) -> str | None: - """1Panel App Store container name (NEXUS_1PANEL_DB_HOST / REDIS_HOST).""" - val = os.environ.get(f"NEXUS_1PANEL_{kind}_HOST", "").strip() - return val or None - - def _service_tcp_targets(port: int, onepanel_kind: str) -> list[tuple[str, int]]: """Probe order: 1Panel container name → host.docker.internal → loopback.""" targets: list[tuple[str, int]] = [] @@ -164,7 +168,7 @@ def _probe_mysql_installed() -> tuple[bool, str]: return _probe_tcp_service(3306, "DB") -def _mysql_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool: +def _service_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool: import socket try: @@ -195,6 +199,27 @@ def _mysql_connect_error_detail(host: str, port: int) -> str: return "" +def _redis_connect_error_detail(host: str, port: int) -> str: + if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1": + return "" + onepanel_redis = _onepanel_service_host("REDIS") + if onepanel_redis and host == onepanel_redis: + return ( + f"无法连接 1Panel Redis 容器 {host}:{port}。" + "请确认:① Redis 容器运行中 ② Nexus 已接入 1panel-network(服务器执行 nx update)" + "③ 若 1Panel Redis 设置了密码,请在步骤 3 填写 redis_pass。" + ) + if host in ("host.docker.internal", "172.17.0.1"): + example = onepanel_redis or "1Panel-redis-xxxx" + return ( + f"无法通过 {host} 连接 Redis。" + "1Panel 应用商店 Redis 在 1panel-network 内,容器间应使用 Redis 容器名" + f"(如 {example}),而非 host.docker.internal。" + "在服务器执行 nx update 可自动接入并预填容器名。" + ) + return "" + + def _parse_dotenv(path: Path) -> dict[str, str]: data: dict[str, str] = {} if not path.is_file(): @@ -228,6 +253,12 @@ async def _test_mysql_url(db_url: str) -> tuple[bool, str]: def _test_redis_url(redis_url: str) -> tuple[bool, str]: if not redis_url: return False, "✗ 未配置 NEXUS_REDIS_URL" + parsed = urlparse(redis_url) + host = parsed.hostname or "127.0.0.1" + port = parsed.port or 6379 + if not _service_tcp_reachable(host, port): + detail = _redis_connect_error_detail(host, port) + return False, detail or f"✗ Redis TCP 不通({host}:{port})" try: import redis as rlib @@ -235,6 +266,11 @@ def _test_redis_url(redis_url: str) -> tuple[bool, str]: client.ping() return True, "✓ Redis 连接正常" except Exception as e: + err = str(e) + if "Connection refused" in err or "Error 111" in err: + detail = _redis_connect_error_detail(host, port) + if detail: + return False, detail return False, f"✗ Redis 连接失败: {e}" @@ -645,14 +681,22 @@ async def init_db(req: InitDbRequest): redis_url = _build_redis_url(req) site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}" db_port = int(req.db_port) + redis_port = int(req.redis_port) - if not _mysql_tcp_reachable(req.db_host, db_port): + if not _service_tcp_reachable(req.db_host, db_port): detail = _mysql_connect_error_detail(req.db_host, db_port) raise HTTPException( 400, detail or f"无法连接 MySQL 服务器 {req.db_host}:{db_port}(TCP 不通,请确认服务已启动)", ) + if not _service_tcp_reachable(req.redis_host, redis_port): + detail = _redis_connect_error_detail(req.redis_host, redis_port) + raise HTTPException( + 400, + detail or f"无法连接 Redis 服务器 {req.redis_host}:{redis_port}(TCP 不通,请确认服务已启动)", + ) + try: patch_aiomysql_do_ping() engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300)