Skip to content

Commit a5f79ed

Browse files
committed
容器:解决 Docker 在 Unraid 首次启动时数据库密码初始化竞态并补充排障文档
1 parent 9921df9 commit a5f79ed

File tree

6 files changed

+108
-13
lines changed

6 files changed

+108
-13
lines changed

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,7 @@ bash start-open-xiaoai-gateway.sh
243243

244244
| 配置项 | 说明 | 默认值 |
245245
|--------|------|--------|
246-
| `FAMILYCLAW_DATABASE_URL` | PostgreSQL 数据库地址 | Docker 首次启动自动生成随机密码,并持久化到 `/data/runtime/secrets/db-password` |
247-
| `FAMILYCLAW_HOME_ASSISTANT_BASE_URL` | Home Assistant 地址 | `http://127.0.0.1:8123` |
248-
| `FAMILYCLAW_HOME_ASSISTANT_TOKEN` | Home Assistant 长期访问令牌 | 需要手动填写 |
249-
| `FAMILYCLAW_AI_DEFAULT_PROVIDER_CODE` | 默认 AI 供应商 | `local-ollama` |
246+
| `FAMILYCLAW_DATABASE_URL` | PostgreSQL 数据库地址 | Docker 默认会把最终使用的数据库密码同步到 `/data/runtime/secrets/db-password`;如需自定义,优先传 `FAMILYCLAW_DB_PASSWORD`,不要让它和连接串里的密码打架 |
250247
| `FAMILYCLAW_VOICE_RUNTIME_MODE` | 语音模式 | `embedded`(本地处理) |
251248

252249
## 插件开发指南
@@ -603,10 +600,7 @@ bash start-open-xiaoai-gateway.sh
603600

604601
| Setting | Description | Default |
605602
|---------|-------------|---------|
606-
| `FAMILYCLAW_DATABASE_URL` | PostgreSQL connection string | Docker auto-generates a random password on first start and stores it in `/data/runtime/secrets/db-password` |
607-
| `FAMILYCLAW_HOME_ASSISTANT_BASE_URL` | Home Assistant URL | `http://127.0.0.1:8123` |
608-
| `FAMILYCLAW_HOME_ASSISTANT_TOKEN` | Home Assistant long-lived access token | Must be set manually |
609-
| `FAMILYCLAW_AI_DEFAULT_PROVIDER_CODE` | Default AI provider | `local-ollama` |
603+
| `FAMILYCLAW_DATABASE_URL` | PostgreSQL connection string | Docker syncs the effective database password to `/data/runtime/secrets/db-password`; if you customize it, prefer `FAMILYCLAW_DB_PASSWORD` and keep it consistent with the password inside the URL |
610604
| `FAMILYCLAW_VOICE_RUNTIME_MODE` | Voice processing mode | `embedded` (local) |
611605

612606
## Plugin Development Guide

docker/scripts/common.sh

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,65 @@ log() {
2828
printf '[%s] %s\n' "${FAMILYCLAW_LOG_PREFIX}" "$1"
2929
}
3030

31+
read_password_from_database_url() {
32+
python - <<'PY'
33+
from __future__ import annotations
34+
35+
import os
36+
from urllib.parse import urlsplit, unquote
37+
38+
database_url = os.environ.get("FAMILYCLAW_DATABASE_URL", "").strip()
39+
if not database_url:
40+
raise SystemExit(0)
41+
42+
password = urlsplit(database_url).password
43+
if password:
44+
print(unquote(password))
45+
PY
46+
}
47+
48+
canonicalize_database_url() {
49+
python - <<'PY'
50+
from __future__ import annotations
51+
52+
import os
53+
from urllib.parse import quote, urlsplit, urlunsplit
54+
55+
database_url = os.environ.get("FAMILYCLAW_DATABASE_URL", "").strip()
56+
password = os.environ["FAMILYCLAW_DB_PASSWORD"]
57+
58+
if not database_url:
59+
user = quote(os.environ.get("FAMILYCLAW_DB_USER", "familyclaw"), safe="")
60+
host = os.environ.get("FAMILYCLAW_DB_HOST", "127.0.0.1")
61+
port = os.environ.get("FAMILYCLAW_DB_PORT", "5432")
62+
database_name = quote(os.environ.get("FAMILYCLAW_DB_NAME", "familyclaw"), safe="/")
63+
encoded_password = quote(password, safe="")
64+
print(f"postgresql+psycopg://{user}:{encoded_password}@{host}:{port}/{database_name}")
65+
raise SystemExit(0)
66+
67+
parts = urlsplit(database_url)
68+
scheme = parts.scheme or "postgresql+psycopg"
69+
username = parts.username or os.environ.get("FAMILYCLAW_DB_USER", "familyclaw")
70+
hostname = parts.hostname or os.environ.get("FAMILYCLAW_DB_HOST", "127.0.0.1")
71+
port = parts.port
72+
path = parts.path or f"/{quote(os.environ.get('FAMILYCLAW_DB_NAME', 'familyclaw'), safe='')}"
73+
74+
userinfo = quote(username, safe="")
75+
if password:
76+
userinfo = f"{userinfo}:{quote(password, safe='')}"
77+
78+
hostpart = hostname
79+
if ":" in hostname and not hostname.startswith("["):
80+
hostpart = f"[{hostname}]"
81+
82+
netloc = f"{userinfo}@{hostpart}"
83+
if port is not None:
84+
netloc = f"{netloc}:{port}"
85+
86+
print(urlunsplit((scheme, netloc, path, parts.query, parts.fragment)))
87+
PY
88+
}
89+
3190
generate_secret() {
3291
local token_bytes="${1:-32}"
3392
python - "${token_bytes}" <<'PY'
@@ -56,12 +115,40 @@ write_secret_file() {
56115
chmod 600 "${secret_file}"
57116
}
58117

118+
acquire_secret_lock() {
119+
local lock_dir="$1"
120+
local max_attempts="${2:-300}"
121+
local attempt=0
122+
local lock_parent
123+
124+
lock_parent="$(dirname "${lock_dir}")"
125+
mkdir -p "${lock_parent}"
126+
127+
while ! mkdir "${lock_dir}" 2>/dev/null; do
128+
attempt=$((attempt + 1))
129+
if [[ "${attempt}" -ge "${max_attempts}" ]]; then
130+
log "Timed out waiting for secret lock ${lock_dir}"
131+
return 1
132+
fi
133+
sleep 0.1
134+
done
135+
}
136+
137+
release_secret_lock() {
138+
local lock_dir="$1"
139+
rmdir "${lock_dir}" 2>/dev/null || true
140+
}
141+
59142
load_or_create_secret() {
60143
local var_name="$1"
61144
local secret_file="$2"
62145
local secret_label="$3"
63146
local token_bytes="${4:-32}"
64147
local current_value="${!var_name:-}"
148+
local lock_dir="${secret_file}.lock"
149+
150+
# 首次启动时多个服务会并发 source 本脚本,这里必须串行化 secret 初始化。
151+
acquire_secret_lock "${lock_dir}"
65152

66153
if [[ -n "${current_value}" ]]; then
67154
write_secret_file "${secret_file}" "${current_value}"
@@ -74,6 +161,8 @@ load_or_create_secret() {
74161
log "Generated and persisted ${secret_label} at ${secret_file}"
75162
fi
76163

164+
release_secret_lock "${lock_dir}"
165+
77166
printf -v "${var_name}" '%s' "${current_value}"
78167
export "${var_name}"
79168
}
@@ -85,10 +174,18 @@ is_truthy() {
85174
esac
86175
}
87176

177+
if [[ -z "${FAMILYCLAW_DB_PASSWORD:-}" && -n "${FAMILYCLAW_DATABASE_URL:-}" ]]; then
178+
parsed_db_password="$(read_password_from_database_url || true)"
179+
if [[ -n "${parsed_db_password}" ]]; then
180+
export FAMILYCLAW_DB_PASSWORD="${parsed_db_password}"
181+
log "Using database password parsed from FAMILYCLAW_DATABASE_URL"
182+
fi
183+
fi
184+
88185
load_or_create_secret FAMILYCLAW_DB_PASSWORD "${FAMILYCLAW_DB_PASSWORD_FILE}" "database password" 24
89186
load_or_create_secret FAMILYCLAW_VOICE_GATEWAY_TOKEN "${FAMILYCLAW_VOICE_GATEWAY_TOKEN_FILE}" "voice gateway token" 32
90187

91-
export FAMILYCLAW_DATABASE_URL="${FAMILYCLAW_DATABASE_URL:-postgresql+psycopg://${FAMILYCLAW_DB_USER}:${FAMILYCLAW_DB_PASSWORD}@${FAMILYCLAW_DB_HOST}:${FAMILYCLAW_DB_PORT}/${FAMILYCLAW_DB_NAME}}"
188+
export FAMILYCLAW_DATABASE_URL="$(canonicalize_database_url)"
92189
export FAMILYCLAW_PLUGIN_STORAGE_ROOT="${FAMILYCLAW_PLUGIN_STORAGE_ROOT:-${FAMILYCLAW_PLUGIN_DATA_DIR}}"
93190
export FAMILYCLAW_PLUGIN_MARKETPLACE_INSTALL_ROOT="${FAMILYCLAW_PLUGIN_MARKETPLACE_INSTALL_ROOT:-${FAMILYCLAW_PLUGIN_DATA_DIR}}"
94191
export FAMILYCLAW_VOICE_RUNTIME_ARTIFACTS_ROOT="${FAMILYCLAW_VOICE_RUNTIME_ARTIFACTS_ROOT:-${FAMILYCLAW_VOICE_ARTIFACTS_DIR}}"

docs/Documentation/en/getting-started/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ docker run -d \
3636

3737
This documentation uses `latest` as the default install entry. Only pin a concrete tag when you need precise rollback, issue reproduction, or a fixed release target.
3838

39-
3. On first start, the container generates a random database password and voice gateway token, then stores them under `/srv/familyclaw-data/runtime/secrets/`. About one minute later, open `http://<server-ip>:8080` in a browser. If the login page appears, the system is up.
39+
3. On first start, the container generates a random database password and voice gateway token, then stores them under `/srv/familyclaw-data/runtime/secrets/`. If you customize the database password, prefer `FAMILYCLAW_DB_PASSWORD`; when `FAMILYCLAW_DATABASE_URL` is also present, the password in both places must stay consistent. About one minute later, open `http://<server-ip>:8080` in a browser. If the login page appears, the system is up.
4040
4. The initial account is `user` / `user`. After login, follow the setup flow to change the account and password.
4141

4242
Placeholder for screenshot: login page after Docker startup

docs/Documentation/en/installation-deployment/docker-installation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ On first start, the container auto-generates a random database password and a ra
4646
- `/data/runtime/secrets/voice-gateway-token`
4747

4848
If you want to take over either value yourself, you can still pass `FAMILYCLAW_DB_PASSWORD` or `FAMILYCLAW_VOICE_GATEWAY_TOKEN`. The container will use your value and sync it back into the same secrets files.
49+
If you also pass `FAMILYCLAW_DATABASE_URL`, the container now syncs the password inside that URL to the same canonical secret. Do not keep different passwords in `FAMILYCLAW_DB_PASSWORD` and the URL, because older images can fail to start that way.
4950

5051
## Verify after startup
5152

@@ -59,7 +60,8 @@ If you want to take over either value yourself, you can still pass `FAMILYCLAW_D
5960

6061
- Cannot reach port 8080: check your firewall or whether another service already uses the port.
6162
- The container does not start: make sure the image pulled successfully, or remove the old container first with `docker rm -f familyclaw`.
62-
- Login shows database errors: confirm `/data/runtime/secrets/db-password` was created and the mounted data volume is writable.
63+
- Login shows database errors: confirm `/data/runtime/secrets/db-password` was created and the mounted data volume is writable. If you customized the database connection, verify that `FAMILYCLAW_DB_PASSWORD` matches the password embedded in `FAMILYCLAW_DATABASE_URL`.
64+
- Fresh Unraid or NAS deployment still shows `password authentication failed for user "familyclaw"`: this is usually not stale data. Older images can hit a first-start race while generating the database password. Update to an image with the fix; if you must stay on the older image for now, explicitly pass the same value in both `FAMILYCLAW_DB_PASSWORD` and `FAMILYCLAW_DATABASE_URL`.
6365
- Voice-related errors while you do not use voice: you can skip the `4399` port mapping and ignore voice gateway logs.
6466

6567
## Uninstall

docs/Documentation/安装部署/Docker安装.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ docker run -d \
4646
- `/data/runtime/secrets/voice-gateway-token`
4747

4848
如果你明确要接管这两个值,仍然可以手工传 `FAMILYCLAW_DB_PASSWORD``FAMILYCLAW_VOICE_GATEWAY_TOKEN`;容器会优先使用你传入的值并同步回上面的 secrets 文件。
49+
如果你还额外传了 `FAMILYCLAW_DATABASE_URL`,容器也会把里面的数据库密码同步成同一个值;不要再让 `FAMILYCLAW_DB_PASSWORD` 和连接串密码写成两个不同值,否则旧版本会直接把自己搞挂。
4950

5051
## 启动后验证
5152

@@ -59,7 +60,8 @@ docker run -d \
5960

6061
- 访问不到 8080:检查防火墙或端口是否被占用。
6162
- 无法启动容器:确认 Docker 拉镜像成功,或清理旧同名容器 `docker rm -f familyclaw`
62-
- 登录后提示数据库错误:先确认 `/data/runtime/secrets/db-password` 已生成,再确认数据卷可写。
63+
- 登录后提示数据库错误:先确认 `/data/runtime/secrets/db-password` 已生成,再确认数据卷可写;如果你自定义过数据库连接,检查 `FAMILYCLAW_DB_PASSWORD``FAMILYCLAW_DATABASE_URL` 里的密码是否一致。
64+
- Unraid / NAS 上全新部署仍然报 `password authentication failed for user "familyclaw"`:这通常不是旧数据没删干净,而是旧镜像在首次启动时撞上了数据库密码初始化竞态。更新到包含修复的镜像;如果暂时只能用旧镜像,先显式传同一个 `FAMILYCLAW_DB_PASSWORD``FAMILYCLAW_DATABASE_URL` 规避。
6365
- 语音相关报错但不用语音:可不映射 4399 端口,忽略语音网关日志。
6466

6567
## 需要卸载

docs/Documentation/快速开始/快速启动.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ docker run -d \
3838

3939
这里默认写 `latest`,让安装文档保持稳定。只有在你要精确回滚、复现旧版本问题,或者明确锁定某个发布版时,才改成具体版本标签。
4040

41-
4. 第一次启动时,系统会自动生成需要的密钥信息,并写入 `/srv/familyclaw-data/runtime/secrets/`
41+
4. 第一次启动时,系统会自动生成需要的密钥信息,并写入 `/srv/familyclaw-data/runtime/secrets/`如果你要自定义数据库密码,优先传 `FAMILYCLAW_DB_PASSWORD`;如果同时传了 `FAMILYCLAW_DATABASE_URL`,两边密码必须一致。
4242
5. 等大约 1 分钟后,在浏览器打开 `http://服务器IP:8080`。如果你已经看到登录页,说明系统已经跑起来了。
4343
6. 初始账号是 `user`,初始密码也是 `user`。第一次登录后,按页面引导继续完成初始化,并尽快改掉默认账号密码。
4444

0 commit comments

Comments
 (0)