Следствие закрытия [books-task-runner-registry-auth-cred]. concepts/registry-oci-image-index-gc.md (new): books-* образы в registry = OCI image-index (buildx), top-level .config=null, дата .created в платформенном sub-manifest. Наивный GC по top-level дате → null у всех → null-dated группы защищаются → drop=0 всегда (keepLastN не применяется). Правила: Accept со всеми media-types, дата из sub, DELETE по index-digest не sub. Зафиксировано на books registryGc dryRun (deleted=0 при 14 tags) + манифест-dump books-web. concepts/bindmount-config-edit-preserve-mode.md: дополнен гочей про bind-mount shadow (config образа затенён целиком → полная секция, не дельта) + worked example task-runner (mode не слетел, постмортем сработал). +index.md (2 строки) +log.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
5.9 KiB
Markdown
67 lines
5.9 KiB
Markdown
---
|
||
title: Правка bind-mounted config'а — сохранять file mode (иначе EACCES crash-loop у non-root контейнера)
|
||
type: concept
|
||
tags: [docker, bind-mount, permissions, gotcha, postmortem, books-vds, eacces]
|
||
updated: 2026-06-18
|
||
---
|
||
|
||
# Bind-mounted config: сохраняй mode при правке
|
||
|
||
Когда правишь config-файл, **bind-mounted в контейнер, который бежит под non-root юзером** (например books `node`, uid 1000), атомарная запись через `mktemp`+`mv` **теряет исходный режим файла** и роняет контейнер в crash-loop с `EACCES`.
|
||
|
||
## Механизм
|
||
|
||
```bash
|
||
jq ... > "$(mktemp)" && mv tmp default.json # ← баг
|
||
```
|
||
`mktemp` создаёт файл `0600 root`. `mv` (rename) **заменяет inode**, перенося права temp-файла на цель — **режим назначения НЕ сохраняется** (в отличие от `cp`/`install`). Исходный был `0644`; после правки `others`-read бит пропал → контейнерный uid 1000 не может открыть файл → падение ещё в загрузчике config'а (node lib `config`, `util.parseFile`), до любого прикладного кода:
|
||
|
||
```
|
||
Error: Config file .../default.json cannot be read. Error code is: EACCES
|
||
```
|
||
|
||
## Правильно
|
||
|
||
Восстановить mode/owner после mv — проще всего по до-правочному бэкапу (`cp -a` его сохраняет):
|
||
```bash
|
||
chmod --reference="$backup" "$f" && chown --reference="$backup" "$f"
|
||
```
|
||
Альтернативы: `install -m 644`, либо запись **в тот же inode** (сохраняет mode):
|
||
```bash
|
||
jq ... > "$f.new" && cat "$f.new" > "$f" && rm "$f.new"
|
||
```
|
||
Всегда `ls -l` после — убедиться, что `others`-read бит на месте для контейнерного uid.
|
||
|
||
## Инцидент 2026-06-18 (worked example)
|
||
|
||
Добавлял `docker.registryAuth` (кред [`books-ci`](registry-kzntsv-auth-model.md)) в `/opt/books/job-scheduler/config/default.json` на [`books-vds`](../entities/books-vds.md) через `jq>mktemp && mv`. Режим слетел `644 → 600 root`. На ближайшем деплое books (`all`-tenant пересоздаёт контейнеры) `books-job-scheduler` ушёл в `Restarting (1)` каждые ~60с — **прод-даун, slovo-cron'ы стояли**. Корень — не код books и не деплой, а мой способ записи.
|
||
|
||
Фикс: `chmod/chown --reference=<backup>` → `-rw-r--r-- root root`. Контейнер сам поднялся на следующей попытке restart-policy (отдельный рестарт не нужен — policy уже крутила). `registryAuth` остался цел.
|
||
|
||
## Связанная гоча: bind-mount затеняет config-каталог образа ЦЕЛИКОМ
|
||
|
||
Books-стеки монтируют host-каталог поверх config-пути образа:
|
||
```
|
||
/opt/books/task-runner/config → /usr/src/app/packages/task-runner/config (rw)
|
||
/opt/books/job-scheduler/config → /usr/src/app/packages/job-scheduler/config
|
||
```
|
||
Bind-mount каталога **полностью заменяет** содержимое целевого каталога — `default.json`, запечённый в образ, **не виден** контейнеру. Читается только host'овый файл.
|
||
|
||
**Следствие при добавлении кред-секции:** нельзя класть «только дельту» (один `password`), полагаясь что `baseUrl`/`username` придут из образа — их там нет, образный `default.json` затенён. **Кладётся полная секция.** На этом спотыкаются, когда таска сформулирована как «в git default.json baseUrl+username уже есть, нужен только password» — в git есть, в эффективном (host) конфиге нет.
|
||
|
||
Проверка эффективного конфига — изнутри контейнера, не по git:
|
||
```bash
|
||
docker exec <ctr> node -e "const c=require('config'); console.log(c.get('registry.baseUrl'))"
|
||
```
|
||
|
||
Host config-каталоги книг от **25.05** (миграция в Portainer) и с тех пор не пере-наливались из образа → дрейфуют от git. node-config читает ровно их. Worked example — `registry`-секция в task-runner (2026-06-18): см. [`registry-oci-image-index-gc`](registry-oci-image-index-gc.md) (кред books-ci для registryGc).
|
||
|
||
## Инцидент-2 2026-06-18 (task-runner, без поломки)
|
||
|
||
Тот же кред-класс (books-ci для [`registryGc`](registry-oci-image-index-gc.md)), но в `/opt/books/task-runner/config/default.json`, секция **`registry`** (не `docker.registryAuth` — другой потребитель). Учтя оба урока выше: бэкап `cp -a` → `jq > .new && cat .new > "$F"` (запись в тот же inode, mode сохранён) → `ls -l` показал `-rw-r--r-- root root`. Структурный diff: добавлен только ключ `registry`. Рестарт контейнера (node-config читает на старте) → `healthy`, EACCES не было. **Mode-гоча не повторилась — постмортем сработал.**
|
||
|
||
## Связанные uid-гочи
|
||
|
||
- [`ocis-on-vds-deploy-recipe`](ocis-on-vds-deploy-recipe.md) — UID 1001 vs 1000 на oCIS (тот же класс: контейнерный uid ≠ root, права host-файлов должны это учитывать).
|
||
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — что за кред клали, когда словили инцидент.
|