--- 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 <'"` — после 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.