fix(install): MySQL TCP probe and clearer host.docker.internal errors

Step 2 now detects whether host MySQL is listening; init-db fails fast with 1Panel setup guidance on error 2003.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Deploy
2026-06-06 05:22:16 +08:00
parent 569263dddb
commit da0424a268
3 changed files with 103 additions and 7 deletions
+70 -5
View File
@@ -138,6 +138,56 @@ def _probe_redis_installed() -> tuple[bool, str]:
return False, f"✗ 未检测到 Redis{hint}"
def _probe_mysql_installed() -> tuple[bool, str]:
"""Step 2: TCP probe — MySQL 是否在宿主机监听(不验证账号密码)。"""
import socket
targets: list[tuple[str, int]] = []
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
targets.append(("host.docker.internal", 3306))
targets.append(("127.0.0.1", 3306))
seen: set[tuple[str, int]] = set()
for host, port in targets:
if (host, port) in seen:
continue
seen.add((host, port))
try:
with socket.create_connection((host, port), timeout=2.0):
return True, f"✓ 已安装({host}:{port} 可访问)"
except OSError:
continue
hint = (
"host.docker.internal:3306"
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
else "127.0.0.1:3306"
)
return False, f"✗ 未检测到 MySQL{hint}"
def _mysql_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool:
import socket
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def _mysql_connect_error_detail(host: str, port: int) -> str:
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
return ""
if host not in ("host.docker.internal", "172.17.0.1"):
return ""
return (
f"容器无法 TCP 连接宿主机 MySQL({host}:{port})。"
"请先在 1Panel 应用商店安装 MySQL 并确保服务运行;"
"创建数据库 nexus 与用户 nexus 后,在安装向导步骤 3 填写密码。"
"服务器上可执行: docker exec nexus-prod-nexus-1 sh -c "
f"'timeout 2 bash -c \"</dev/tcp/{host}/{port}\"' 验证端口。"
)
def _parse_dotenv(path: Path) -> dict[str, str]:
data: dict[str, str] = {}
if not path.is_file():
@@ -521,12 +571,14 @@ async def env_check():
"blocks_wizard": False,
})
# MySQL — 步骤3配置,步骤4验证连接
# MySQL — 步骤2 TCP 检测;步骤4验证账号密码
mysql_installed, mysql_msg = _probe_mysql_installed()
checks.append({
"name": "MySQL 数据库",
"required": "步骤3配置",
"current": "— 步骤4将检测连接",
"pass": True,
"name": "MySQL(宿主机)",
"required": "建议已安装",
"current": mysql_msg,
"pass": mysql_installed,
"blocks_wizard": False,
})
# Write permissions
@@ -583,6 +635,14 @@ async def init_db(req: InitDbRequest):
db_url = _make_db_url(req)
redis_url = _build_redis_url(req)
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
db_port = int(req.db_port)
if not _mysql_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 不通,请确认服务已启动)",
)
try:
patch_aiomysql_do_ping()
@@ -673,6 +733,11 @@ async def init_db(req: InitDbRequest):
except Exception as e:
await engine.dispose()
err = str(e)
if "2003" in err or "Can't connect to MySQL" in err:
extra = _mysql_connect_error_detail(req.db_host, db_port)
if extra:
raise HTTPException(400, extra) from e
raise HTTPException(400, f"数据库初始化失败: {e}") from e
await engine.dispose()