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:
Nexus Deploy
2026-06-06 05:35:04 +08:00
parent da0424a268
commit 6951b8049f
9 changed files with 215 additions and 66 deletions
+25 -12
View File
@@ -93,28 +93,41 @@ nexus-install # 等同 quick-install.sh
---
## MySQL / Redis自行安装
## MySQL / Redis1Panel 应用商店
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 容器:
+27
View File
@@ -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
View File
@@ -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-networkMySQL/Redis 使用 1Panel 容器名互联)"
fi
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
info "构建并启动(首次约 1020 分钟)..."
compose_cmd "$root" up -d --build --remove-orphans
+5 -1
View File
@@ -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 访问
# 1Panelnx 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
+15
View File
@@ -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
+2
View File
@@ -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}
+2 -2
View File
@@ -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
View File
@@ -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",
}