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
+14 -2
View File
@@ -95,14 +95,26 @@ nexus-install # 等同 quick-install.sh
## MySQL / Redis(自行安装)
Docker 栈**仅含 Nexus 容器**。
Docker 栈**仅含 Nexus 容器**。Nexus 容器通过 `host.docker.internal` 访问宿主机上的 MySQL/Redis。
| 服务 | 宿主机 | 安装向导步骤 3 |
|------|--------|----------------|
| MySQL | `127.0.0.1:3306` | `host.docker.internal:3306` |
| Redis | `127.0.0.1:6379` | `host.docker.internal:6379` |
步骤 2 仅检测 Redis 是否已安装;步骤 3 填连接信息;步骤 4 验证 MySQL/Redis 连通。
### 1Panel 安装 MySQL(步骤 3 之前必做)
1. **应用商店** → 安装 **MySQL**(保持默认,会暴露宿主机 `3306`
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
3. 安装向导步骤 3:主机保持预填的 `host.docker.internal`,库名/用户名 `nexus`,填入上一步密码
若报 `Can't connect to MySQL server on 'host.docker.internal'`,通常是 **MySQL 尚未安装或未启动**。在服务器验证:
```bash
docker exec nexus-prod-nexus-1 sh -c 'timeout 2 bash -c "</dev/tcp/host.docker.internal/3306"' && echo MySQL_OK
```
步骤 2 会 TCP 检测 MySQL/Redis 是否已安装;步骤 4 再验证账号密码与 Redis 连通。
卸载旧 Compose 容器:
@@ -0,0 +1,19 @@
# 2026-06-05 — 安装向导 MySQL TCP 检测与 2003 错误说明
## 摘要
Docker 部署下步骤 3 报 `(2003) Can't connect to MySQL server on 'host.docker.internal'` 多为宿主机未安装/未启动 MySQL。增加步骤 2 MySQL TCP 探测、init-db 前置 TCP 检查与可读错误文案;README-1panel 补充 1Panel 建库步骤。
## 动机
外置 MySQL 需用户在 1Panel 应用商店自行安装;原向导步骤 2 不检测 MySQL,步骤 3 直接 pymysql 报错难以理解。
## 涉及文件
- `server/api/install.py``_probe_mysql_installed`、init-db TCP 预检
- `deploy/README-1panel.md` — 1Panel MySQL 安装与验证命令
## 验证
- 未装 MySQL:步骤 2 显示「未检测到 MySQL」;步骤 3 返回中文说明而非裸 pymysql 栈
- 已装 MySQL:步骤 2 显示可访问;步骤 3 可正常建表
+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()