# Deployment — zmienne środowiskowe i produkcja Jedno źródło prawdy o zmiennych środowiskowych (wszystkie, co znaczą, wymagane czy nie) oraz jak wdrożyć projekt na produkcję spójnie. Env jest częścią deploymentu — te same zmienne w dev (.env) i na produkcji (runtime hostingu). Powiązane: [getting-started.md](./getting-started.md), [storage.md](./storage.md) (R2), [email.md](./email.md) (Graph), [security.md](./security.md). --- ## 1. ZMIENNE ŚRODOWISKOWE — pełna lista ### Rdzeń (WYMAGANE — projekt bez nich nie wstanie) ```bash # Baza danych (Mongo albo Postgres — zależnie od projektu) DATABASE_URI=mongodb://... # albo postgres://... / file:./dev.db (dev) # Sekret Payload (podpisywanie sesji/tokenów) — losowy, długi PAYLOAD_SECRET= # Publiczny URL serwisu (canonical, hreflang, OG, manifest) NEXT_PUBLIC_SERVER_URL=https://klient.pl # dev: http://localhost:3000 ``` ### Email — Graph (OPCJONALNE, agencyjne, gdy transport = Graph) ```bash GRAPH_TENANT_ID= GRAPH_CLIENT_ID= GRAPH_CLIENT_SECRET= GRAPH_SENDER=noreply@mailservice.intecion.net # wspólna skrzynka ``` Bez nich transport Graph nie zadziała (fallback SMTP). Patrz email.md. ### Storage — R2 (OPCJONALNE, gdy media w R2) ```bash R2_BUCKET= R2_ENDPOINT=https://.r2.cloudflarestorage.com R2_ACCESS_KEY_ID= R2_SECRET_ACCESS_KEY= R2_PUBLIC_URL=https://media.klient.pl # custom domena (obrazy publiczne) ``` Brak → fallback na lokalny dysk. Patrz storage.md. ### Tabela — wszystkie zmienne | Zmienna | Wymagana | Warstwa | Opis | |---|---|---|---| | `DATABASE_URI` | ✅ | infra | połączenie z bazą | | `PAYLOAD_SECRET` | ✅ | infra | sekret Payload | | `NEXT_PUBLIC_SERVER_URL` | ✅ | infra | publiczny URL (canonical, OG) | | `GRAPH_TENANT_ID` | ⬜ | email | Azure tenant (Graph) | | `GRAPH_CLIENT_ID` | ⬜ | email | Azure app id | | `GRAPH_CLIENT_SECRET` | ⬜ | email | Azure secret | | `GRAPH_SENDER` | ⬜ | email | skrzynka nadawcza | | `R2_BUCKET` | ⬜ | storage | bucket R2 | | `R2_ENDPOINT` | ⬜ | storage | endpoint S3 R2 | | `R2_ACCESS_KEY_ID` | ⬜ | storage | klucz R2 | | `R2_SECRET_ACCESS_KEY` | ⬜ | storage | sekret R2 | | `R2_PUBLIC_URL` | ⬜ | storage | custom domena mediów | **Zasada:** wszystkie sekrety to zmienne agencyjne/infrastrukturalne — w `.env` (dev) i runtime hostingu (prod), NIGDY w repo. Dane per-projekt edytowalne przez redaktora idą do PANELU, nie do env (patrz architektura-tresci.md). ### .env.example — zawsze w repo Każdy projekt ma `.env.example` z listą zmiennych (bez wartości/sekretów) — szablon dla następnej osoby. Commituj go; `.env` (z wartościami) NIGDY. --- ## 2. PRZED DEPLOYEM — checklist - [ ] `pnpm build --webpack` przechodzi LOKALNIE (nie tylko dev) - [ ] Wszystkie wymagane env ustawione na hostingu (runtime) - [ ] `NEXT_PUBLIC_SERVER_URL` = produkcyjny URL (nie localhost) - [ ] `PAYLOAD_SECRET` inny niż w dev (produkcyjny sekret) - [ ] Baza produkcyjna (nie dev/SQLite) - [ ] HSTS włączony (buildSecurityHeaders hsts: production) - [ ] Media: R2 z custom domeną (jeśli używane) — obrazy publiczne - [ ] Migracja mediów lokalne→R2 (jeśli przełączasz) - [ ] Strony polityk + baner cookies (patrz wymagania-prawne.md) --- ## 3. BUDOWANIE NA PRODUKCJĘ ### Build script (Next 16 + Payload) ```json // package.json — --webpack KONIECZNE (Turbopack konfliktuje z withPayload) "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" next build --webpack" ``` `--max-old-space-size` — Payload + Next bywają pamięciożerne przy buildzie; 3072 MB zapobiega OOM na mniejszych maszynach. ### Kolejność build → migracje → start ```bash pnpm install --frozen-lockfile # dokładnie z lockfile (powtarzalny build) pnpm generate:types # typy z kolekcji pnpm build # --webpack # migracje bazy (jeśli Postgres z migracjami): pnpm payload migrate pnpm start # produkcyjny serwer ``` --- ## 3a. Pułapka: metadata w zamiast (htmlLimitedBots) Next 16 streamuje metadata dynamicznych stron do `` (przenosi do head skryptem JS). Crawlery bez JS widzą canonical/hreflang/title/favicon poza head → utrata SEO. **Każdy projekt** tego potrzebuje w next.config: ```ts const nextConfig: NextConfig = { htmlLimitedBots: /Googlebot|Google-InspectionTool|Bingbot|Yandex|DuckDuckBot|Screaming Frog|AhrefsBot|SemrushBot/i, // ... } ``` Weryfikacja: `curl -A "Googlebot" URL | grep canonical` — musi być w ``. Szczegóły i objawy: seo.md (sekcja htmlLimitedBots). ## 3a2. ISR — cache stron (metadata w head + szybkość) Dla stron contentowych (page.tsx) użyj ISR: `export const revalidate = 3600`. Cache'uje całą stronę z gotowym `` → metadata zawsze w head (nie body, brak race condition streamingu), TTFB ~20ms, brak 503 (cold start). ```ts // app/(frontend)/[locale]/[[...slug]]/page.tsx export const revalidate = 3600 // 1h; albo krócej, albo on-demand ``` UWAGA: ISR i `force-dynamic` się WYKLUCZAJĄ. Strony → ISR (revalidate). sitemap/robots → force-dynamic (bo generują przy żądaniu). Nie mieszaj na jednej trasie. Treść z panelu: ISR = redaktor czeka do rewalidacji; rozważ on-demand revalidation (hook afterChange → revalidatePath). Patrz seo.md, HOOKS.md. ## 3a3. SSG a dostęp do bazy przy buildzie (WAŻNE dla SEO) `generateStaticParams` (z lib/content) prerenderuje strony jako SSG — head synchroniczny, SEO 100/100. ALE żeby prerenderować, **build musi mieć dostęp do bazy** (generateStaticParams czyta strony z bazy w czasie buildu). - **Build MA dostęp do bazy** (baza w tej samej sieci Docker, dostępna w build stage) → strony prerenderowane jako SSG (`●`), head synchroniczny → SEO OK ✓ - **Build NIE MA dostępu** (izolowany build stage) → generateStaticParams zwraca `[]` (plugin łapie błąd, build nie pada), ale strony renderują się on-demand (dynamicznie) → head może streamować do body → problem SEO wraca ✗ Plugin zabezpiecza build przed CRASHEM (try/catch → `[]`), ale to NIE zastępuje dostępu do bazy. **Dla pełnego SSG/SEO zapewnij, że build kontenerowy widzi bazę.** W Coolify/Docker: baza (Mongo/Postgres) powinna być dostępna podczas `pnpm build`, nie tylko w runtime. Jeśli build jest w izolowanej sieci — rozważ: - uruchom bazę w tej samej sieci Docker co build stage, albo - build z DATABASE_URI wskazującym na dostępną bazę (nie wewnętrzny host niedostępny w buildzie). Weryfikacja: po buildzie `pnpm build` pokazuje trasy jako `●` (SSG), nie `ƒ` (Dynamic). Jeśli `ƒ` mimo generateStaticParams → build nie miał dostępu do bazy. ## 3b. Pułapka: prerender tras zależnych od bazy (KONIECZNE) Next domyślnie **prerenderuje** trasy typu `sitemap.ts` w czasie `next build` — traktuje je jako statyczne. Jeśli taka trasa czyta bazę (sitemap → Payload → Mongo/Postgres), build **próbuje połączyć się z bazą**. A kontener budujący (Coolify/Docker/Railway/CI) zwykle NIE ma dostępu do sieci bazy → połączenie pada (`ENOTFOUND`, `MongooseServerSelectionError`) → **build się wywala**. **Rozwiązanie — `force-dynamic` na trasach zależnych od bazy:** ```ts // app/sitemap.ts export { sitemap as default } from '@/lib/content' export const dynamic = 'force-dynamic' // generuj w runtime, nie w buildzie ``` To mówi Next: nie prerenderuj w buildzie, generuj w runtime (gdy baza jest dostępna). Dotyczy KAŻDEJ trasy czytającej bazę podczas renderowania: - `app/sitemap.ts` → `force-dynamic` - inne trasy/strony czytające bazę w prerenderze → rozważ `force-dynamic` albo obsłuż błąd bazy (try/catch z fallbackiem) Plugin dodatkowo zabezpiecza handler sitemap (łapie błąd bazy, zwraca pustą mapę), więc build nie padnie nawet bez `force-dynamic` — ale to siatka bezpieczeństwa, nie właściwe rozwiązanie. Zawsze dodawaj `force-dynamic`. **Strona 404** (`not-found.tsx`) czytająca ustawienia z bazy — ten sam problem. Opakuj `getCachedPayload()` w try/catch, żeby brak bazy w buildzie nie wywalił prerenderu 404 (fallback na statyczne teksty). **Weryfikacja lokalna** (symuluj brak bazy): ```bash DATABASE_URI=mongodb://invalid-host:27017/test pnpm build # build musi przejść (kod 0), mimo niedostępnej bazy ``` ## 4. HOSTING (Coolify / Docker) ### Zmienne runtime, nie build Zmienne środowiskowe ustaw w **runtime** hostingu (Coolify → Environment Variables), nie zapiekaj w build. `NEXT_PUBLIC_*` są wyjątkiem — wchodzą w build (bo publiczne, w bundlu klienta), więc muszą być dostępne PODCZAS buildu. ### Persystencja mediów Jeśli media lokalne (nie R2) — potrzebują **wolumenu** (inaczej znikną przy redeployu). Dlatego R2 jest zalecane na produkcji: media poza kontenerem, przetrwają redeploy. Patrz storage.md. ### Health check Payload wystawia panel pod `/admin` — health check może pingować stronę główną albo `/admin`. Nie ustawiaj health check na endpoint wymagający bazy, jeśli baza wstaje wolniej niż app. --- ## 5. PO DEPLOYU — weryfikacja - [ ] Strona główna `/` przekierowuje na locale (`/pl`) - [ ] Panel `/admin` działa, logowanie OK - [ ] Formularz wysyła (test przez panel: Send test) - [ ] Media się wyświetlają (jeśli R2 — custom domena działa, nie 403) - [ ] Favicon w `` (patrz seo.md — Google cache'uje wolno) - [ ] HTTPS + nagłówki bezpieczeństwa (sprawdź np. securityheaders.com) - [ ] Sitemap `/sitemap.xml` i `/robots.txt` odpowiadają --- ## DLACZEGO TO WAŻNE - **Jedna lista env** — nikt nie zgaduje, czego brakuje - **Powtarzalny deploy** — frozen-lockfile, ta sama kolejność, każdy projekt tak samo - **Sekrety bezpieczne** — env/runtime, nigdy repo - **Media przetrwają** — R2 albo wolumen, nie znikają przy redeployu