fix(beat): use /tmp/celerybeat-schedule (workdir /app not writable by app user) #226

Merged
lekss361 merged 1 commit from fix/beat-celerybeat-schedule-permission into main 2026-05-16 19:06:27 +00:00
Owner

Problem

Beat container в restart-loop после PR #225 force-recreate. GlitchTip Issues заполняется одной и той же ошибкой:

beat raised exception <class '_gdbm.error'>: error(13, 'Permission denied')
  Permission denied: 'celerybeat-schedule'
  celery/beat.py in _open_schedule at line 523
  shelve.py in open at line 243
  dbm/__init__.py in open at line 95

Beat не работает → все scheduled tasks НЕ запускаются:

  • scrape_kn (DOM.РФ еженедельный)
  • scrape_objective_sync (Объектив еженедельный)
  • scrape_ekburg_permits_sync (РНС/РВЭ ежемесячный)
  • scrape_nspd_geo, scrape_pzz, scrape_poi, scrape_noise
  • refresh аналитики MV

Root cause

  • backend/Dockerfile runner stage: WORKDIR /app создаётся под root → owner root:root.
  • COPY --chown=app:app переносит файлы (.venv/, app/, alembic/, alembic.ini) под app, но саму директорию /app/ не chown'ит.
  • USER appapp (uid 1000) не может создавать новые файлы в /app/ (нет write permission на parent dir).
  • До PR #225 beat container не пересоздавался force-recreate, файл /app/celerybeat-schedule существовал в writable форме из inherited state (предположительно от первого запуска под root). Force-recreate из PR #225 сбросил состояние.

Fix

--schedule=/tmp/celerybeat-schedule/tmp всегда writable. Schedule-файл хранит только last_run_at для periodic tasks; потеря на container restart OK, beat перестраивает из celery_app.conf.beat_schedule на старте.

Не делаю в этом PR

  • Permanent Dockerfile fix (chown app:app /app в runner stage). Runner stage шарится с FastAPI backend image (uvicorn) и worker — риск регрессии больше чем у однострочного --schedule override. Можно отдельным PR после verify что beat работает.

Test plan

  • Deploy зелёный после merge
  • docker compose -p gendesign logs beat --tail 50 — нет Permission denied, есть beat: Starting...
  • GlitchTip Issues: новых событий _gdbm.error нет
  • Через 1-2 минуты после deploy: beat heartbeat в GlitchTip monitoring (CeleryIntegration(monitor_beat_tasks=True))
  • Через несколько минут: одна из scheduled тасок отстреливает (но cron'ы редкие — основное — отсутствие новых Permission denied)

References

  • PR #225 (cause: force-recreate)
  • vault decisions/Dec_GlitchTip_Frontend_Sentry_SDK.md (event capture confirmed working)
## Problem Beat container в restart-loop после PR #225 force-recreate. GlitchTip Issues заполняется одной и той же ошибкой: ``` beat raised exception <class '_gdbm.error'>: error(13, 'Permission denied') Permission denied: 'celerybeat-schedule' celery/beat.py in _open_schedule at line 523 shelve.py in open at line 243 dbm/__init__.py in open at line 95 ``` **Beat не работает** → все scheduled tasks НЕ запускаются: - `scrape_kn` (DOM.РФ еженедельный) - `scrape_objective_sync` (Объектив еженедельный) - `scrape_ekburg_permits_sync` (РНС/РВЭ ежемесячный) - `scrape_nspd_geo`, `scrape_pzz`, `scrape_poi`, `scrape_noise` - refresh аналитики MV ## Root cause - `backend/Dockerfile` runner stage: `WORKDIR /app` создаётся под root → owner `root:root`. - `COPY --chown=app:app` переносит файлы (`.venv/`, `app/`, `alembic/`, `alembic.ini`) под `app`, но **саму директорию `/app/` не chown'ит**. - `USER app` → `app` (uid 1000) не может создавать новые файлы в `/app/` (нет write permission на parent dir). - До PR #225 beat container не пересоздавался force-recreate, файл `/app/celerybeat-schedule` существовал в writable форме из inherited state (предположительно от первого запуска под root). Force-recreate из PR #225 сбросил состояние. ## Fix `--schedule=/tmp/celerybeat-schedule` — `/tmp` всегда writable. Schedule-файл хранит только `last_run_at` для periodic tasks; потеря на container restart OK, beat перестраивает из `celery_app.conf.beat_schedule` на старте. ## Не делаю в этом PR - **Permanent Dockerfile fix** (`chown app:app /app` в runner stage). Runner stage шарится с FastAPI backend image (uvicorn) и worker — риск регрессии больше чем у однострочного `--schedule` override. Можно отдельным PR после verify что beat работает. ## Test plan - [ ] Deploy зелёный после merge - [ ] `docker compose -p gendesign logs beat --tail 50` — нет `Permission denied`, есть `beat: Starting...` - [ ] GlitchTip Issues: новых событий `_gdbm.error` нет - [ ] Через 1-2 минуты после deploy: beat heartbeat в GlitchTip monitoring (CeleryIntegration(monitor_beat_tasks=True)) - [ ] Через несколько минут: одна из scheduled тасок отстреливает (но cron'ы редкие — основное — отсутствие новых Permission denied) ## References - PR #225 (cause: force-recreate) - vault decisions/Dec_GlitchTip_Frontend_Sentry_SDK.md (event capture confirmed working)
lekss361 added 1 commit 2026-05-16 19:04:17 +00:00
Beat был в restart-loop после force-recreate в PR #225 deploy:
  beat raised exception <class '_gdbm.error'>: error(13, 'Permission denied')
  shelve.py / dbm/__init__.py — попытка создать 'celerybeat-schedule' в CWD.

Root cause
- backend/Dockerfile runner stage: WORKDIR /app создаётся под root → owner root:root.
- COPY --chown=app:app переносит файлы под app, но саму директорию /app не chown'ит.
- USER app → app (uid 1000) не может создавать новые файлы в /app/ (нет write
  permission на parent dir).
- До PR #225 beat container не пересоздавался force-recreate, файл schedule
  существовал в writable forme из inherited state. Force-recreate сбросил.

Fix
- --schedule=/tmp/celerybeat-schedule — /tmp всегда writable.
- Schedule-файл хранит только last_run_at для periodic tasks; потеря на
  container restart OK, beat перестраивает из celery_app.conf.beat_schedule.

Permanent fix (отдельный PR — runner Dockerfile)
- Добавить `chown app:app /app` в runner stage после WORKDIR. Не делаю в
  этом PR — хотфикс минимальный, runner стейдж шарится с FastAPI image и
  worker, риск регрессии больше чем у однострочного --schedule override.
lekss361 merged commit e4852b0ec9 into main 2026-05-16 19:06:27 +00:00
Author
Owner

APPROVE — merged via deep-code-reviewer.

Findings

Fix корректен. Дефолт celery beat → ./celerybeat-schedule относительно cwd = /app (per Dockerfile WORKDIR /app). /app owned root:root (создан до useradd app), COPY --chown=app:app chown'ит только содержимое, не сам каталог → uid 1000 не может создать новый файл. /tmp всегда 1777 в python:3.12-slim (стандартный Debian base), tmpfs не смонтирован на beat → файл сохранится между beat-restart'ами в пределах одного container lifetime, потеряется при up -d --force-recreate — это OK.

Idempotency проверена. Потеря shelve = потеря last_run_at. На старте build_beat_schedule() (см. backend/app/workers/beat_schedule.py) перестраивает расписание из job_settings БД (с env fallback). Все cron-таски в codebase идемпотентные (scrape_kn, objective_sync, refresh_analytics, nspd-geo-zombie-cleanup, poi-sync-weekly, noise-sync-weekly, ekburg-permits-monthly) — повторный запуск после restart безопасен.

Single-file scope. Только docker-compose.prod.yml → триггерит deploy.yml (см. .claude/rules/deploy.md). Worker / backend / Caddyfile не тронуты. Нет read_only: true / tmpfs override на beat — /tmp writable.

Anti-regression. В vault meta/00_credentials.md ошибка уже была отмечена как pre-existing issue ("known Celery beat schedule file perms issue") — этот PR её закрывает. Никаких --no-verify / force-push в истории ветки.

Tradeoff (acceptable)

/tmp ephemeral между recreate'ами → таска с week+ cron может выстрелить лишний раз после redeploy. Для всех текущих cron-таск это безопасно (idempotent scrapers/refresh).

Recommendation для permanent fix (отдельный PR, не блокирующий)

В backend/Dockerfile runner stage добавить RUN mkdir -p /app && chown app:app /app до WORKDIR /app, либо USER rootchown -R app:app /appUSER app после COPY. Тогда beat сможет писать schedule в default location, и поведение между recreate'ами станет deterministic (shelve переживёт перезапуск container'а с тем же image). Затронет shared image (uvicorn + worker + beat) — нужен smoke-test, но риск низкий (chown пустой dir → no-op для backend/worker, write для beat).

Vault update note

После verify зелёного deploy — обнови meta/00_credentials.md (убрать "Pre-existing issue" про beat) и добавь fixes/Fix_Beat_CelerybeatSchedule_Tmp_May16.md per CLAUDE.md rule #6.

Merge commit: e4852b0.

**APPROVE** — merged via deep-code-reviewer. ### Findings **Fix корректен.** Дефолт celery beat → `./celerybeat-schedule` относительно cwd = `/app` (per Dockerfile `WORKDIR /app`). `/app` owned `root:root` (создан до `useradd app`), `COPY --chown=app:app` chown'ит только содержимое, не сам каталог → uid 1000 не может создать новый файл. `/tmp` всегда `1777` в `python:3.12-slim` (стандартный Debian base), tmpfs не смонтирован на beat → файл сохранится между beat-restart'ами в пределах одного container lifetime, потеряется при `up -d --force-recreate` — это OK. **Idempotency проверена.** Потеря shelve = потеря `last_run_at`. На старте `build_beat_schedule()` (см. `backend/app/workers/beat_schedule.py`) перестраивает расписание из `job_settings` БД (с env fallback). Все cron-таски в codebase идемпотентные (`scrape_kn`, `objective_sync`, `refresh_analytics`, `nspd-geo-zombie-cleanup`, `poi-sync-weekly`, `noise-sync-weekly`, `ekburg-permits-monthly`) — повторный запуск после restart безопасен. **Single-file scope.** Только `docker-compose.prod.yml` → триггерит `deploy.yml` (см. `.claude/rules/deploy.md`). Worker / backend / Caddyfile не тронуты. Нет `read_only: true` / tmpfs override на beat — `/tmp` writable. **Anti-regression.** В vault `meta/00_credentials.md` ошибка уже была отмечена как pre-existing issue ("known Celery beat schedule file perms issue") — этот PR её закрывает. Никаких `--no-verify` / force-push в истории ветки. ### Tradeoff (acceptable) `/tmp` ephemeral между recreate'ами → таска с week+ cron может выстрелить лишний раз после redeploy. Для всех текущих cron-таск это безопасно (idempotent scrapers/refresh). ### Recommendation для permanent fix (отдельный PR, не блокирующий) В `backend/Dockerfile` runner stage добавить `RUN mkdir -p /app && chown app:app /app` **до** `WORKDIR /app`, либо `USER root` → `chown -R app:app /app` → `USER app` после COPY. Тогда beat сможет писать schedule в default location, и поведение между recreate'ами станет deterministic (shelve переживёт перезапуск container'а с тем же image). Затронет shared image (uvicorn + worker + beat) — нужен smoke-test, но риск низкий (chown пустой dir → no-op для backend/worker, write для beat). ### Vault update note После verify зелёного deploy — обнови `meta/00_credentials.md` (убрать "Pre-existing issue" про beat) и добавь `fixes/Fix_Beat_CelerybeatSchedule_Tmp_May16.md` per CLAUDE.md rule #6. Merge commit: `e4852b0`.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#226
No description provided.