Incident: W4 PM2 Crash-Loop + Inline Preview Broken (2026-07-06)

PoleWartość
IDINCIDENT-2026-07-06-001
PlatformaW4 (w4.pinbox24.com, bms-1, ns367522)
SevernośćP0 (aplikacja crash-loop, inline preview 403)
StatusRESOLVED — crash-loop fixed 2026-07-06T07:00Z; inline preview fix 2026-07-06T14:30Z
Wykryto2026-07-06T~03:00Z
Rozwiązano (crash)2026-07-06T~07:00Z
Rozwiązano (inline preview)2026-07-06T~14:30Z (session 49)
PR#3015 (MERGED)

Symptomy

  1. PM2 crash-loop: oba workery v42-prod_backend restartowały co kilka sekund (restart count >10)
  2. Inline preview plików 403: GET /api/offices/files/:fileId/getSignedUrl generował URL podpisany skasowanym kluczem Wasabi → AccessDenied
  3. Sensor cron noise: co minutę w logach $$$ Cron for Sensor test data every 1 min
  4. WSDL missing: ENOENT: .../dist/wsdl/UslugaBIRzewnPubl-ver11-test.wsdl przy wywołaniach GUS API

Oś czasu

Czas (UTC)Zdarzenie
~03:00PM2 crash-loop wykryty — restart co kilka sekund
~04:00Root cause crash: TypeError: Cannot read property 'phoneNumbers' of undefined w socketInitUser.helper.js
~04:30Null guard dodany do connectWithPhoneNumbers — dwa przypadki: userProfileDoc null + channelList undefined
~05:00Sensor cron wyłączony w app.js (komentarz)
~05:12Obraz v42-prod:merged-20260706-0512 zbudowany przez drugi session Claude (łączy getSignedUrl + wszystkie patche)
~05:30Obraz uruchomiony z błędnym entrypoint (sleep infinity zamiast docker-entrypoint.sh) → PM2 nie startuje
~05:45Fix entrypoint: docker run --entrypoint docker-entrypoint.sh ... → PM2 online
~06:00WSDL brakujący → skopiowany z overlay /var/lib/docker/overlay2/.../diff/app/node_modules/...
~06:15getSignedUrl endpoint dostępny: HTTP 200
~06:30Test inline preview: 403 Forbidden z Wasabi — klucz AJ00**** (s3v2-prod-bms1) nie ma dostępu do bucketu pinbox24
~07:00Analiza IAM: prawidłowy user to pinbox24-bms1-s3 (klucze WKRQ****, LGQ2****) — oczekuje na autoryzację rotacji

Root causes

1. PM2 crash-loop — phoneNumbers TypeError

Plik: v42-prod:/app/dist/globalHelpers/socketHelper/socketInitUser.helper.js

Funkcja connectWithPhoneNumbers wywoływała userData.channelList.phoneNumbers.map(...) bez sprawdzenia czy channelList istnieje. Jeśli user nie miał channelList po pętli przez userProfileDoc.myConnectedChannel, wartość była undefined.

Dwa null-case:

  • userProfileDoc — null z MongoDB (user usunięty lub nie znaleziony)
  • userData.channelList — undefined gdy myConnectedChannel jest puste

2. Merged image — błędny entrypoint

Drugi Claude session zbudował obraz z kontenera uruchomionego komendą sleep 3600. Przy docker commit ENTRYPOINT stał się ["/usr/bin/sleep"], CMD ["3600"]. PM2 nigdy nie startował.

3. WSDL missing

Paczka @pobidowski/gus-api-regon ma plik WSDL w src/wsdl/, ale skompilowany kod oczekuje go w dist/wsdl/. Brakowało kroku kopiowania przy buildzie.

4. Inline preview 403 — skasowany klucz Wasabi

backend-environment.env miał s3Bucket_api_accessKeyId=VCUC7X6A1G... (skasowany podczas rotacji 2026-07-01). Podmieniono na AJ00**** (s3v2-prod-bms1) ale ten user nie ma dostępu do prywatnego bucketu pinbox24.

Prawidłowy IAM user: pinbox24-bms1-s3 z kluczami WKRQ**** i LGQ2****.


Zmienione pliki / kontenery

W kontenerze v42-prod (obraz v42-prod:merged-20260706-0512)

PlikZmianaRollback
/app/dist/globalHelpers/socketHelper/socketInitUser.helper.jsDwa null guardy w connectWithPhoneNumbersPrzywróć backend-environment.env.bak.pre-bms-rs0-20260706044245 i zrestartuj z poprzednim obrazem
/app/dist/app.jsWykomentowany officeCron_helper_1.sensorTestData()Odkomentuj linie cron
/app/dist/middleware/socket.middleware.jssetTimeout w try-catch + null guard dla app_1.io.sockets[socket.id](był już w poprzednim session)
/app/dist/config/redis.config.jsHost 172.18.0.9:6379 hardcoded (redis-v42 container)Zmień z powrotem na env var OVH Valkey
/app/dist/globalHelpers/socketHelper/socket.helper.jsPort 6379, host "172.18.0.9", TLS/auth usuniętePrzywróć config Valkey
/app/dist/apps/files/file.route.jsNowa trasa GET /api/offices/files/:fileId/getSignedUrlUsuń route
/app/dist/apps/files/file.controller.jsexports.getSignedUrl — wywołuje helperUsuń controller method
/app/dist/apps/files/file.helper.jsgetSignedUrlHelper — szuka FileModel, wywołuje s3getSignedUrlUsuń helper method
/app/dist/globalHelpers/uploadAwsS3.helper.jss3getSignedUrl — generuje signed URL z ResponseContentDisposition: 'inline', 30min expiryUsuń funkcję
/app/dist/wsdl/UslugaBIRzewnPubl-ver11-test.wsdlSkopiowany z overlay do dist/wsdl/Usuń plik (zrestartuj)

Na serwerze bms-1

PlikZmianaRollback
/home/p24-server-scripts/v4/v42/backend-environment.envs3Bucket_api_accessKeyId=AJ00**** (tymczasowy, zły bucket)Przywróć bak.pre-bms-rs0-20260706044245
/root/patch-v42.shSkrypt re-apply wszystkich 7 patchy po docker recreateUsuń plik
Redis container redis-v42Nowy kontener Redis 7 Alpine na 172.18.0.9:6379, sieć prod-v-4-netdocker stop redis-v42 && docker rm redis-v42

Weryfikacja fix (PM2 crash)

docker exec v42-prod pm2 list
# → 2x v42-prod_backend: online, cluster mode
 
curl -s http://localhost:3000/api/health
# → HTTP 200
 
docker exec v42-prod pm2 logs --nostream --lines 50 | grep -c "phoneNumbers"
# → 0 (brak crash)

RESOLVED: inline preview fix (session 49 — 2026-07-06T~14:30Z)

Rotacja klucza IAM Wasabi zakończona pomyślnie:

KrokStatusSzczegóły
Usuń stary kluczURH4**** usunięty (secret był utracony z poprzedniej próby)
Utwórz nowy kluczB7MW**** dla user pinbox24-bms1-s3
Zaktualizuj SOPSsecrets/pinbox24-w4.env.sops — V42_s3Bucket_api_accessKeyId + secret zaktualizowane, canary OK, PR #3015 MERGED
Zaktualizuj bms-1/home/p24-server-scripts/v4/v42/backend-environment.env s3Bucket_api_accessKeyId=B7MW****; backup .bak.s3rot-* utworzony
Reload PM2docker exec v42-prod pm2 reload all --update-env — oba workery online
Test S3aws s3 ls s3://pinbox24/ --endpoint-url https://s3.eu-central-1.wasabisys.com → 14 obiektów dostępnych
Health checkbash /root/health_check_v42.sh → exit 0, ALL OK

Pozostałe dziś w nocy (po 20:00):

  • Otwórz w4.pinbox24.com w przeglądarce → plik → Preview → PDF powinien otworzyć się inline (weryfikacja w przeglądarce)
  • Usuń stary klucz WKRQ**** z Wasabi IAM dla pinbox24-bms1-s3 (po potwierdzeniu inline preview)

Health check script: /root/health_check_v42.sh na bms-1 (też w repo: scripts/health_check_v42.sh)

Runbook rotacji: docs/pinbox24/runbook-rotate-pinbox24-bms1-s3-key.md


Długoterminowy fix (całość)

  1. Klucz S3 w SOPS: Wypełnij V42_s3Bucket_api_accessKeyId + V42_s3Bucket_api_secretAccessKey w secrets/pinbox24-w4.env.sops z wartościami IAM user pinbox24-bms1-s3
  2. Baked image: Zamroź obraz v42-prod:merged-20260706-0512 jako v42-prod:stable-20260706 z:
    • Poprawnym entrypoint (docker-entrypoint.sh)
    • Wszystkimi patchami wbudowanymi (nie tylko w kontenerze)
    • patch-v42.sh uwzględniający WSDL copy
  3. Redis trwały: Dodaj redis-v42 do docker-compose.yml dla v42
  4. CI/CD: Zaktualizuj pipeline żeby patch był częścią budowania obrazu, nie hotfixem
  5. Monitoring: Alert na PM2 restart count > 3 w ciągu 5 minut

Lekcje

  1. docker commit na kontenerze uruchomionym komendą sleep zapisuje zły entrypoint — zawsze budować image przez docker build lub commitować z --change "ENTRYPOINT ..."
  2. Rotacja kluczy Wasabi musi obejmować WSZYSTKICH konsumentów danego bucketu przed usunięciem starego klucza
  3. secrets/pinbox24-w4.env.sops miał puste wartości dla V42_s3Bucket_api_accessKeyId — SOPS jest bezużyteczny jako disaster recovery jeśli wartości są puste
  4. Null guard to nie luksus — każde .map() na potencjalnie undefined musi mieć guard
  5. WSDL i inne static files muszą być częścią buildu Docker, nie ręcznego cp