206 lines
7.7 KiB
Markdown
206 lines
7.7 KiB
Markdown
# 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=<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)
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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)
|
|
|
|
```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 <body> 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:
|
|
|
|
```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 `<head>`.
|
|
Szczegóły i objawy: seo.md (sekcja htmlLimitedBots).
|
|
|
|
## 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 `<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 |