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:
+14
-2
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user