VDS Rusonyx 160 NVMe (89.253.255.94 / vds.kzntsv.site) активирован 2026-05-20. 3 фазы за ~6 часов: - Phase 1: harden + docker 29.5.1 + traefik v2.11 + portainer 2.21.5 - Phase 2: shared DB park (postgres 16 / mariadb 11.4 / mongo 7 / redis 7) via traefik raw TCP forward + self-signed TLS - Phase 3: миграция gitea (132 repos), verdaccio (2063 pkgs), registry fresh install (user accepted loss old images) + Joxit GUI Ingest 1 source + 1 entity (vds-kzntsv) + 6 concepts (traefik-tcp- passthrough-vs-starttls / portainer-2.21-admin-password-regression / db-tls-self-signed-via-traefik-raw-tcp / rusonyx-vps-onboarding-quirks / registry-gc-mount-and-modify-flag / compose-bcrypt-escape-trap). 3 follow-up ⚪ tasks: vds-gc-cron, vds-backup-rsync-kreknin, vds-ntfy-push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.5 KiB
title, type, tags, sources, updated
| title | type | tags | sources | updated | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Docker Compose bcrypt `$` escape trap | concept |
|
|
2026-05-20 |
Docker Compose ест $ в bcrypt hashes
Симптом
services:
app:
command: --admin-password '$2y$05$abc123...'
При docker compose up Compose выдаёт warning:
warning msg="The \"r9BLFM98iCLTskjgj7Y3teOACut3Nxk\" variable is not set. Defaulting to a blank string."
И в контейнер передаётся пустая строка вместо bcrypt hash.
Root cause
Compose interpolates ${VAR} и $VAR syntax из env variables в YAML values. Bcrypt hash начинается с $2y$ или $2a$ — выглядит как $NAME шаблоны для compose'а:
$2y→ попытка expand env var2y→ не определена → empty string$05→ expand env var05→ empty string- Остаток до точки/конца строки → имя var → empty
- Точки/слэши прерывают имя var
Результат — обрезанный/пустой hash.
Решение 1 — double-escape $ → $$
В YAML literal compose treats $$ как escape для $. Нужно sed-replace перед записью:
BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2)
BCRYPT_ESCAPED=$(echo "$BCRYPT" | sed 's/\$/\$\$/g')
cat > docker-compose.yml <<COMPOSE
services:
app:
command: --admin-password '$BCRYPT_ESCAPED'
COMPOSE
docker compose config покажет $$2y$$05$$... — это нормально, в runtime expand'ится в $2y$05$....
Но осторожно с кавычками: YAML string form command: "... '<hash>'" — после tokenization shell видит literal одинарные кавычки внутри значения. Argument приходит как '$2y$05$...' (с кавычками вокруг hash) → bcrypt verify fails из-за паразитных ' char'ов.
Решение 2 — YAML list form
command:
- "--admin-password"
- "$$2y$$05$$abc123..."
Каждый list-element — отдельный argv element, без shell tokenization. Кавычки не утекают.
Но опять же — некоторые версии Compose (2024+) могут иметь регрессии где YAML list form не unescape'ит $$ обратно в $. Проверять через docker compose config | grep command после написания.
Решение 3 — docker run напрямую, без compose
Bypass проблему. Shell escape'ы (одинарные кавычки) предсказуемы:
BCRYPT=$(htpasswd -nbB user pass | cut -d':' -f2)
docker run -d --name app \
-v ... \
app/app:tag \
--admin-password "$BCRYPT" # bash interpolation одного слоя
Bash "$BCRYPT" expand'ится один раз. Внутри expansion'а $ уже не парсится. Container получает чистый hash.
Trade-off: лишаемся compose файла → нет declarative restart, нужно docker run --restart unless-stopped. Управляемо.
Решение 4 — env file with single var
services:
app:
env_file: ./bcrypt.env
command: --admin-password ${BCRYPT}
bcrypt.env:
BCRYPT=$2y$05$abc123...
Env file читается raw, без compose interpolation. Тогда ${BCRYPT} в command expand'ится из этого env. Работает.
Но менее transparent — debugger должен смотреть в env file.
Похожие traps
Любые значения с $:
- Postgres password
secret$pass$word - API keys с
$(rare) - Cron syntax в command (
$1etc.) - Bash variable substitutions внутри command
Где применено
portainer/portainer-ce:2.21.5 на vds-kzntsv — финальный путь = решение 3 (docker run direct). Хотя в этой же сессии оказалось что --admin-password flag broken в Portainer 2.21+ regardless escape — см. portainer-2.21-admin-password-regression.
Ссылки
- Compose docs: Variable interpolation
- Сессия:
vds-kzntsv-bootstrap-2026-05-20Phase 1 cont.