git-subtree-dir: .tmp-concepts git-subtree-mainline:98bcc37d32git-subtree-split:c727aaa1b6
117 lines
4.5 KiB
Markdown
117 lines
4.5 KiB
Markdown
---
|
||
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.
|