Files
admin/.wiki/concepts/compose-bcrypt-escape-trap.md

4.5 KiB
Raw Blame History

title, type, tags, sources, updated
title type tags sources updated
Docker Compose bcrypt `$` escape trap concept
docker-compose
bcrypt
password
escape
gotcha
../sources/vds-kzntsv-bootstrap-2026-05-20.md
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 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 перед записью:

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 ($1 etc.)
  • 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.

Ссылки