Files

9.8 KiB

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, storage.md (R2), email.md (Graph), security.md.


1. ZMIENNE ŚRODOWISKOWE — pełna lista

Rdzeń (WYMAGANE — projekt bez nich nie wstanie)

# 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=<losowy-ciąg-min-32-znaki>

# 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)

GRAPH_TENANT_ID=<azure-tenant-id>
GRAPH_CLIENT_ID=<azure-app-client-id>
GRAPH_CLIENT_SECRET=<azure-app-secret>
GRAPH_SENDER=[email protected]   # wspólna skrzynka

Bez nich transport Graph nie zadziała (fallback SMTP). Patrz email.md.

Storage — R2 (OPCJONALNE, gdy media w R2)

R2_BUCKET=<nazwa-bucketa>
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<access-key>
R2_SECRET_ACCESS_KEY=<secret-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)

// 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

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 <head> (htmlLimitedBots)

Next 16 streamuje metadata dynamicznych stron do <body> (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:

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 <head>. 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 <head> → metadata zawsze w head (nie body, brak race condition streamingu), TTFB ~20ms, brak 503 (cold start).

// 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:

// 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):

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 <head> (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