From b25d0798f68d2ffa42a60c8e02cbdbece1ccf92f Mon Sep 17 00:00:00 2001 From: Nexus Deploy Date: Sat, 6 Jun 2026 05:47:43 +0800 Subject: [PATCH] fix(1panel): sync MySQL/Redis container hosts on nx update Upgrade was skipping 1panel-network detection; wizard now overrides stale host.docker.internal from volume/API defaults. Co-authored-by: Cursor --- .gitignore | 1 + deploy/detect-1panel-services.sh | 28 +++++++++++--- deploy/nexus-1panel.sh | 37 ++++++++++++++++++- .../2026-06-05-1panel-hosts-upgrade-sync.md | 26 +++++++++++++ server/api/install.py | 22 ++++++++++- web/app/install.html | 34 ++++++++++++++++- 6 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 docs/changelog/2026-06-05-1panel-hosts-upgrade-sync.md diff --git a/.gitignore b/.gitignore index 146aab4a..77c49c26 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ web/uploads/ # Data (sensitive) web/data/config.php web/data/*.db +web/data/1panel-hosts.json # Backups backups/ diff --git a/deploy/detect-1panel-services.sh b/deploy/detect-1panel-services.sh index 3bc06101..becac4bc 100644 --- a/deploy/detect-1panel-services.sh +++ b/deploy/detect-1panel-services.sh @@ -5,9 +5,25 @@ set -euo pipefail -detect_1panel_container() { - local prefix="$1" - docker ps --format '{{.Names}}' 2>/dev/null | grep -E "^${prefix}" | head -1 || true +network_container_names() { + docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \ + | tr ' ' '\n' | sed '/^$/d' +} + +pick_service_container() { + local kind="$1" + local names line + names="$(network_container_names)" + if [[ -z "$names" ]]; then + docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true + return + fi + line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)" + if [[ -n "$line" ]]; then + echo "$line" + return + fi + echo "$names" | grep -i "$kind" | head -1 || true } if ! docker network inspect 1panel-network >/dev/null 2>&1; then @@ -15,11 +31,11 @@ if ! docker network inspect 1panel-network >/dev/null 2>&1; then exit 1 fi -MYSQL_HOST="$(detect_1panel_container '1Panel-mysql-')" -REDIS_HOST="$(detect_1panel_container '1Panel-redis-')" +MYSQL_HOST="$(pick_service_container mysql)" +REDIS_HOST="$(pick_service_container redis)" if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then - echo "No 1Panel MySQL/Redis containers running" >&2 + echo "No MySQL/Redis containers on 1panel-network" >&2 exit 2 fi diff --git a/deploy/nexus-1panel.sh b/deploy/nexus-1panel.sh index 5050ee6a..de0592db 100755 --- a/deploy/nexus-1panel.sh +++ b/deploy/nexus-1panel.sh @@ -200,12 +200,39 @@ upsert_env_var() { fi } +write_1panel_hosts_volume() { + local mysql_host="${1:-}" redis_host="${2:-}" + [[ -n "$mysql_host" || -n "$redis_host" ]] || return 0 + local vol json + vol="$(docker volume ls -q --filter name=nexus-web-data 2>/dev/null | head -1 || true)" + if [[ -z "$vol" ]]; then + warn "未找到 nexus-web-data 卷,跳过写入 1panel-hosts.json" + return 0 + fi + json="{" + if [[ -n "$mysql_host" ]]; then + json+="\"db_host\":\"${mysql_host}\"" + [[ -n "$redis_host" ]] && json+="," + fi + if [[ -n "$redis_host" ]]; then + json+="\"redis_host\":\"${redis_host}\"" + fi + json+="}" + if printf '%s' "$json" | docker run --rm -i -v "${vol}:/data" alpine:3.19 \ + sh -c 'cat > /data/1panel-hosts.json && chmod 644 /data/1panel-hosts.json'; then + info "已写入卷 ${vol} → web/data/1panel-hosts.json" + else + warn "写入 1panel-hosts.json 失败(向导仍可读 .env.prod 环境变量)" + 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 + warn "未找到 1panel-network — 请先在 1Panel 应用商店安装 MySQL/Redis" return 0 fi if [[ ! -x "$detect" ]]; then @@ -215,7 +242,11 @@ sync_1panel_service_hosts() { warn "未找到 detect-1panel-services.sh,跳过 1Panel 容器名探测" return 0 fi - local line mysql_host redis_host + local line mysql_host="" redis_host="" detect_out + if ! detect_out="$("$detect" 2>&1)"; then + warn "1Panel 容器探测: ${detect_out:-无 MySQL/Redis 容器运行}" + return 0 + fi while IFS= read -r line; do case "$line" in NEXUS_1PANEL_DB_HOST=*) @@ -230,7 +261,8 @@ sync_1panel_service_hosts() { info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 已同步)" ;; esac - done < <("$detect" 2>/dev/null || true) + done <<<"$detect_out" + write_1panel_hosts_volume "$mysql_host" "$redis_host" } compose_cmd() { @@ -852,6 +884,7 @@ cmd_upgrade() { apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")" save_host_profile "$NEXUS_ROOT" purge_legacy_bundled_services "$NEXUS_ROOT" + sync_1panel_service_hosts "$NEXUS_ROOT" if [[ "$NO_CACHE" == true ]]; then step "无缓存重建 Nexus 镜像..." compose_cmd "$NEXUS_ROOT" build --no-cache nexus diff --git a/docs/changelog/2026-06-05-1panel-hosts-upgrade-sync.md b/docs/changelog/2026-06-05-1panel-hosts-upgrade-sync.md new file mode 100644 index 00000000..e47f8d0b --- /dev/null +++ b/docs/changelog/2026-06-05-1panel-hosts-upgrade-sync.md @@ -0,0 +1,26 @@ +# 2026-06-05 — 修复 nx update 未同步 1Panel 容器名 + +## 摘要 + +用户步骤 3 仍填 `host.docker.internal`:`cmd_upgrade` 未调用 `sync_1panel_service_hosts`,容器环境变量与向导预填未更新。升级路径补 sync;探测改读 `1panel-network`;写入 `web/data/1panel-hosts.json` 作后备;向导加载时覆盖陈旧主机名。 + +## 根因 + +- `compose_up`(安装)会 sync,`cmd_upgrade`(日常 `nx update`)之前不会 +- 安装状态/表单可能缓存 `host.docker.internal`,覆盖 `docker_defaults` + +## 涉及文件 + +- `deploy/nexus-1panel.sh` — upgrade 调 sync + 写卷 +- `deploy/detect-1panel-services.sh` — 从 1panel-network 枚举容器 +- `server/api/install.py` — 读 `1panel-hosts.json` 后备 +- `web/app/install.html` — `refreshDockerDefaults` 覆盖陈旧主机 + +## 验证 + +```bash +nx update --no-cache +grep 1PANEL docker/.env.prod +docker exec nexus-prod-nexus-1 cat /app/web/data/1panel-hosts.json +# 安装向导步骤 3 db_host 应为 1Panel-mysql-xxxx +``` diff --git a/server/api/install.py b/server/api/install.py index ae57eb62..5f893d39 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -33,6 +33,7 @@ ENV_FILE = ROOT_DIR / ".env" INSTALL_LOCK = ROOT_DIR / ".install_locked" CONFIG_DIR = ROOT_DIR / "web" / "data" CONFIG_JSON = CONFIG_DIR / "config.json" +ONEPANEL_HOSTS_JSON = CONFIG_DIR / "1panel-hosts.json" STATE_FILE = ROOT_DIR / ".install_state.json" @@ -98,10 +99,27 @@ def _build_redis_url(req: InitDbRequest) -> str: return url +def _read_1panel_hosts_file() -> dict[str, str]: + if not ONEPANEL_HOSTS_JSON.is_file(): + return {} + import json + + try: + raw = json.loads(read_utf8_text(ONEPANEL_HOSTS_JSON)) + except (json.JSONDecodeError, OSError): + return {} + if not isinstance(raw, dict): + return {} + return {str(k): str(v) for k, v in raw.items() if v} + + def _onepanel_service_host(kind: str) -> str | None: - """1Panel App Store container name (NEXUS_1PANEL_DB_HOST / REDIS_HOST).""" + """1Panel App Store container name (env → web/data/1panel-hosts.json).""" val = os.environ.get(f"NEXUS_1PANEL_{kind}_HOST", "").strip() - return val or None + if val: + return val + file_key = "db_host" if kind == "DB" else "redis_host" + return _read_1panel_hosts_file().get(file_key) or None def _resolve_redis_probe() -> tuple[str, int, str | None]: diff --git a/web/app/install.html b/web/app/install.html index 430c0c22..98da6af9 100644 --- a/web/app/install.html +++ b/web/app/install.html @@ -628,6 +628,35 @@ function installWizard() { return results.every(r => !r.includes('✗') && !r.includes('⚠')); }, + isStaleDockerHost(host) { + const h = (host || '').trim().toLowerCase(); + return !h || h === 'localhost' || h === '127.0.0.1' || h === 'host.docker.internal'; + }, + + applyDockerDefaults(defaults) { + if (!defaults) return; + if (this.isStaleDockerHost(this.form.db_host) && defaults.db_host) { + this.form.db_host = defaults.db_host; + } + if (this.isStaleDockerHost(this.form.redis_host) && defaults.redis_host) { + this.form.redis_host = defaults.redis_host; + } + if (!this.form.db_name || this.form.db_name === 'Nexus') this.form.db_name = defaults.db_name || this.form.db_name; + if (!this.form.db_user || this.form.db_user === 'Nexus') this.form.db_user = defaults.db_user || this.form.db_user; + if (defaults.db_port) this.form.db_port = defaults.db_port; + if (defaults.redis_port) this.form.redis_port = defaults.redis_port; + if (defaults.redis_db) this.form.redis_db = defaults.redis_db; + }, + + async refreshDockerDefaults() { + try { + const r = await fetch(API + '/api/install/env-check'); + if (!r.ok) return; + const d = await r.json(); + this.applyDockerDefaults(d.docker_defaults); + } catch (e) {} + }, + async init() { // Auto-detect site URL const proto = location.protocol === 'https:' ? 'https' : 'http'; @@ -658,6 +687,9 @@ function installWizard() { if (d.step >= 4) this.checkConnections(); } } catch(e) {} + + // 1Panel: override stale host.docker.internal with container names from API/volume + await this.refreshDockerDefaults(); }, async checkConnections() { @@ -690,7 +722,7 @@ function installWizard() { this.envChecks = d.checks; this.envAllPass = d.all_pass; if (d.docker_defaults) { - Object.assign(this.form, d.docker_defaults); + this.applyDockerDefaults(d.docker_defaults); } } } catch(e) {