fix(1panel): join 1panel-network and connect MySQL/Redis by container name
Per 1Panel App Store architecture, bridge apps must use Docker DNS on 1panel-network instead of host.docker.internal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+25
-12
@@ -93,28 +93,41 @@ nexus-install # 等同 quick-install.sh
|
||||
|
||||
---
|
||||
|
||||
## MySQL / Redis(自行安装)
|
||||
## MySQL / Redis(1Panel 应用商店)
|
||||
|
||||
Docker 栈**仅含 Nexus 容器**。Nexus 容器通过 `host.docker.internal` 访问宿主机上的 MySQL/Redis。
|
||||
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
|
||||
|
||||
| 服务 | 宿主机 | 安装向导步骤 3 |
|
||||
|------|--------|----------------|
|
||||
| MySQL | `127.0.0.1:3306` | `host.docker.internal:3306` |
|
||||
| Redis | `127.0.0.1:6379` | `host.docker.internal:6379` |
|
||||
### 1Panel 官方互联方式(必读)
|
||||
|
||||
### 1Panel 安装 MySQL(步骤 3 之前必做)
|
||||
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`。
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**(保持默认,会暴露宿主机 `3306`)
|
||||
| 场景 | 正确主机 |
|
||||
|------|----------|
|
||||
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
|
||||
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
|
||||
| OpenResty → 应用 | `127.0.0.1:端口`(OpenResty 为 host 网络) |
|
||||
|
||||
`nx update` / `nexus-1panel.sh` 会:
|
||||
|
||||
1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`)
|
||||
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`
|
||||
3. 安装向导步骤 3 **预填容器名**
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. 安装向导步骤 3:主机保持预填的 `host.docker.internal`,库名/用户名 `nexus`,填入上一步密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis 若设了密码一并填写
|
||||
|
||||
若报 `Can't connect to MySQL server on 'host.docker.internal'`,通常是 **MySQL 尚未安装或未启动**。在服务器验证:
|
||||
若仍报 `Can't connect to MySQL server on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
|
||||
|
||||
```bash
|
||||
docker exec nexus-prod-nexus-1 sh -c 'timeout 2 bash -c "</dev/tcp/host.docker.internal/3306"' && echo MySQL_OK
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
步骤 2 会 TCP 检测 MySQL/Redis 是否已安装;步骤 4 再验证账号密码与 Redis 连通。
|
||||
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
|
||||
|
||||
卸载旧 Compose 容器:
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
|
||||
# See: https://github.com/1Panel-dev/1Panel/issues/11676
|
||||
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
detect_1panel_container() {
|
||||
local prefix="$1"
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -E "^${prefix}" | head -1 || true
|
||||
}
|
||||
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
echo "1panel-network not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_HOST="$(detect_1panel_container '1Panel-mysql-')"
|
||||
REDIS_HOST="$(detect_1panel_container '1Panel-redis-')"
|
||||
|
||||
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
|
||||
echo "No 1Panel MySQL/Redis containers running" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
|
||||
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
|
||||
+55
-1
@@ -34,6 +34,7 @@ NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
|
||||
COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
ENV_PROD="docker/.env.prod"
|
||||
HOST_PROFILE_FILE="docker/.host-profile"
|
||||
|
||||
@@ -186,6 +187,51 @@ apply_pool_to_env_prod() {
|
||||
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
||||
}
|
||||
|
||||
has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
upsert_env_var() {
|
||||
local file="$1" key="$2" val="$3"
|
||||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||||
else
|
||||
echo "${key}=${val}" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_1panel_service_hosts() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local detect="$root/deploy/detect-1panel-services.sh"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
if ! has_1panel_network; then
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
chmod +x "$detect" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
warn "未找到 detect-1panel-services.sh,跳过 1Panel 容器名探测"
|
||||
return 0
|
||||
fi
|
||||
local line mysql_host redis_host
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_1PANEL_DB_HOST=*)
|
||||
mysql_host="${line#NEXUS_1PANEL_DB_HOST=}"
|
||||
upsert_env_var "$env_file" NEXUS_1PANEL_DB_HOST "$mysql_host"
|
||||
info "1Panel MySQL 容器: $mysql_host(安装向导将预填为数据库主机)"
|
||||
;;
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
done < <("$detect" 2>/dev/null || true)
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
local root="$1"
|
||||
shift
|
||||
@@ -196,7 +242,11 @@ compose_cmd() {
|
||||
exit 1
|
||||
fi
|
||||
cd "$root"
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
local -a compose_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$COMPOSE_1PANEL" ]]; then
|
||||
compose_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
docker compose "${compose_args[@]}" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
}
|
||||
|
||||
rand_secret() {
|
||||
@@ -555,6 +605,10 @@ compose_up() {
|
||||
local root="$1"
|
||||
save_host_profile "$root"
|
||||
purge_legacy_bundled_services "$root"
|
||||
sync_1panel_service_hosts "$root"
|
||||
if has_1panel_network; then
|
||||
info "已接入 1panel-network(MySQL/Redis 使用 1Panel 容器名互联)"
|
||||
fi
|
||||
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
|
||||
info "构建并启动(首次约 10~20 分钟)..."
|
||||
compose_cmd "$root" up -d --build --remove-orphans
|
||||
|
||||
@@ -18,7 +18,11 @@ NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# 外置 Redis(宿主机/1Panel),Nexus 容器经 host.docker.internal 访问
|
||||
# 1Panel:nx update 自动探测并写入(安装向导步骤 3 预填)
|
||||
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
|
||||
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
|
||||
|
||||
# 外置 Redis(非 1Panel 时用 host.docker.internal)
|
||||
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||
|
||||
# Optional
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 1Panel overlay — join Nexus to 1panel-network (App Store MySQL/Redis use this network).
|
||||
# Applied automatically when `docker network inspect 1panel-network` succeeds (deploy/nexus-1panel.sh).
|
||||
|
||||
services:
|
||||
nexus:
|
||||
networks:
|
||||
- 1panel-network
|
||||
environment:
|
||||
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
|
||||
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
name: 1panel-network
|
||||
@@ -27,6 +27,8 @@ services:
|
||||
NEXUS_PORT: "8600"
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://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}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
|
||||
|
||||
@@ -88,13 +88,13 @@ trap persist_env_on_exit EXIT INT TERM
|
||||
|
||||
# MySQL / Redis 均由用户自行安装在宿主机;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_MYSQL_WAIT:-0}" != "1" ]; then
|
||||
MYSQL_HOST="${MYSQL_HOST:-host.docker.internal}"
|
||||
MYSQL_HOST="${NEXUS_1PANEL_DB_HOST:-${MYSQL_HOST:-host.docker.internal}}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
wait_tcp_optional "${MYSQL_HOST}" "${MYSQL_PORT}" 30
|
||||
fi
|
||||
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
|
||||
REDIS_HOST="${REDIS_HOST:-host.docker.internal}"
|
||||
REDIS_HOST="${NEXUS_1PANEL_REDIS_HOST:-${REDIS_HOST:-host.docker.internal}}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 2026-06-05 — 1Panel 1panel-network + 容器名互联
|
||||
|
||||
## 摘要
|
||||
|
||||
依据 [1Panel #11676](https://github.com/1Panel-dev/1Panel/issues/11676) 与 [appstore mysql/redis](https://github.com/1Panel-dev/appstore):应用商店 MySQL/Redis 在 `1panel-network`,桥接容器应使用**容器名**而非 `host.docker.internal`。Nexus 自动接入该网络并探测 `1Panel-mysql-*` / `1Panel-redis-*` 预填安装向导。
|
||||
|
||||
## 动机
|
||||
|
||||
用户步骤 3 报 `(2003) Can't connect to MySQL server on 'host.docker.internal'`:1Panel MySQL 未映射到 docker 网桥可达地址,或仅监听容器网络。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `docker/docker-compose.1panel.yml` — 接入 `1panel-network`
|
||||
- `deploy/detect-1panel-services.sh` — 探测容器名
|
||||
- `deploy/nexus-1panel.sh` — compose 叠加与 env 写入
|
||||
- `server/api/install.py` — 探测顺序与向导预填
|
||||
- `deploy/README-1panel.md`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
# 安装向导步骤 3 db_host 应为 1Panel-mysql-xxxx
|
||||
```
|
||||
+59
-50
@@ -112,56 +112,56 @@ def _resolve_redis_probe() -> tuple[str, int, str | None]:
|
||||
return host, port, None
|
||||
|
||||
|
||||
def _probe_redis_installed() -> tuple[bool, str]:
|
||||
"""Step 2: TCP probe only — Redis 是否在宿主机监听(不验证密码/库号)。"""
|
||||
import socket
|
||||
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]] = []
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
targets.append(("host.docker.internal", 6379))
|
||||
targets.append(("127.0.0.1", 6379))
|
||||
host = _onepanel_service_host(onepanel_kind)
|
||||
if host:
|
||||
targets.append((host, port))
|
||||
targets.append(("host.docker.internal", port))
|
||||
targets.append(("127.0.0.1", port))
|
||||
seen: set[tuple[str, int]] = set()
|
||||
for host, port in targets:
|
||||
if (host, port) in seen:
|
||||
ordered: list[tuple[str, int]] = []
|
||||
for item in targets:
|
||||
if item in seen:
|
||||
continue
|
||||
seen.add((host, port))
|
||||
seen.add(item)
|
||||
ordered.append(item)
|
||||
return ordered
|
||||
|
||||
|
||||
def _probe_tcp_service(port: int, onepanel_kind: str) -> tuple[bool, str]:
|
||||
import socket
|
||||
|
||||
for host, p in _service_tcp_targets(port, onepanel_kind):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=2.0):
|
||||
return True, f"✓ 已安装({host}:{port} 可访问)"
|
||||
with socket.create_connection((host, p), timeout=2.0):
|
||||
return True, f"✓ 已安装({host}:{p} 可访问)"
|
||||
except OSError:
|
||||
continue
|
||||
hint = (
|
||||
"host.docker.internal:6379"
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
|
||||
else "127.0.0.1:6379"
|
||||
)
|
||||
return False, f"✗ 未检测到 Redis({hint})"
|
||||
hint_host = _onepanel_service_host(onepanel_kind)
|
||||
if not hint_host and os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
hint_host = "host.docker.internal"
|
||||
if not hint_host:
|
||||
hint_host = "127.0.0.1"
|
||||
return False, f"✗ 未检测到服务({hint_host}:{port})"
|
||||
|
||||
|
||||
def _probe_redis_installed() -> tuple[bool, str]:
|
||||
"""Step 2: TCP probe only — Redis 是否可访问(不验证密码/库号)。"""
|
||||
return _probe_tcp_service(6379, "REDIS")
|
||||
|
||||
|
||||
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})"
|
||||
"""Step 2: TCP probe — MySQL 是否可访问(不验证账号密码)。"""
|
||||
return _probe_tcp_service(3306, "DB")
|
||||
|
||||
|
||||
def _mysql_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool:
|
||||
@@ -177,15 +177,22 @@ def _mysql_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool:
|
||||
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}\"' 验证端口。"
|
||||
)
|
||||
onepanel_db = _onepanel_service_host("DB")
|
||||
if onepanel_db and host == onepanel_db:
|
||||
return (
|
||||
f"无法连接 1Panel MySQL 容器 {host}:{port}。"
|
||||
"请确认:① MySQL 容器运行中 ② Nexus 已接入 1panel-network(服务器执行 nx update)"
|
||||
"③ 已在 1Panel 创建库 nexus 与用户 nexus。"
|
||||
)
|
||||
if host in ("host.docker.internal", "172.17.0.1"):
|
||||
example = onepanel_db or "1Panel-mysql-xxxx"
|
||||
return (
|
||||
f"无法通过 {host} 连接 MySQL。"
|
||||
"1Panel 应用商店 MySQL 在 1panel-network 内,容器间应使用 MySQL 容器名"
|
||||
f"(如 {example}),而非 host.docker.internal。"
|
||||
"在服务器执行 nx update 可自动接入网络并预填容器名。"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_dotenv(path: Path) -> dict[str, str]:
|
||||
@@ -235,12 +242,14 @@ 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":
|
||||
return None
|
||||
db_host = _onepanel_service_host("DB") or "host.docker.internal"
|
||||
redis_host = _onepanel_service_host("REDIS") or "host.docker.internal"
|
||||
return {
|
||||
"db_host": "host.docker.internal",
|
||||
"db_host": db_host,
|
||||
"db_port": "3306",
|
||||
"db_name": "nexus",
|
||||
"db_user": "nexus",
|
||||
"redis_host": "host.docker.internal",
|
||||
"redis_host": redis_host,
|
||||
"redis_port": "6379",
|
||||
"redis_db": "0",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user