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 |
⬜ | Azure tenant (Graph) | |
GRAPH_CLIENT_ID |
⬜ | Azure app id | |
GRAPH_CLIENT_SECRET |
⬜ | Azure secret | |
GRAPH_SENDER |
⬜ | 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 --webpackprzechodzi LOKALNIE (nie tylko dev)- Wszystkie wymagane env ustawione na hostingu (runtime)
NEXT_PUBLIC_SERVER_URL= produkcyjny URL (nie localhost)PAYLOAD_SECRETinny 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-dynamicalbo 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
/admindział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.xmli/robots.txtodpowiadają
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