docs(.wiki,.tasks): vds-kzntsv-bootstrap done — 3 phases shipped

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>
This commit is contained in:
2026-05-20 22:39:31 +03:00
parent ec9177dddb
commit dd7c56742e
7 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
---
title: Docker Compose bcrypt `$` escape trap
type: concept
tags: [docker-compose, bcrypt, password, escape, gotcha]
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
updated: 2026-05-20
---
# Docker Compose ест `$` в bcrypt hashes
## Симптом
```yaml
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 var `2y` → не определена → empty string
- `$05` → expand env var `05` → empty string
- Остаток до точки/конца строки → имя var → empty
- Точки/слэши прерывают имя var
Результат — обрезанный/пустой hash.
## Решение 1 — double-escape `$` → `$$`
В YAML literal compose treats `$$` как escape для `$`. Нужно sed-replace перед записью:
```bash
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
```yaml
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'ы (одинарные кавычки) предсказуемы:
```bash
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
```yaml
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 (`$1` etc.)
- Bash variable substitutions внутри command
## Где применено
`portainer/portainer-ce:2.21.5` на [`vds-kzntsv`](../entities/vds-kzntsv.md) — финальный путь = решение 3 (`docker run` direct). Хотя в этой же сессии оказалось что `--admin-password` flag broken в Portainer 2.21+ regardless escape — см. [`portainer-2.21-admin-password-regression`](portainer-2.21-admin-password-regression.md).
## Ссылки
- Compose docs: [Variable interpolation](https://docs.docker.com/compose/compose-file/12-interpolation/)
- Сессия: [`vds-kzntsv-bootstrap-2026-05-20`](../sources/vds-kzntsv-bootstrap-2026-05-20.md) Phase 1 cont.