Scaffold webu na Payload CMS 3 + Next.js nad SQLite
Kostra prezentace s vlastní administrací pro Martina Reinera. Obsah: - kolekce Products (obrázky, volné parametry, cena, dostupnost, skrytí), Media se zmenšeninami, Pages, Users bez veřejné registrace - veřejná část: výpis nabídky, detail položky, statické stránky - /api/health pro Docker HEALTHCHECK Provoz: - Dockerfile, docker-compose.yml (vývoj) a docker-compose.prod.yml (nasazení z Gitea registry) - CI v Gitea Actions: lint -> testy -> build -> push image - úvodní migrace databáze Stránky jsou force-dynamic, aby se úprava položky projevila hned; schéma se za běhu nedomýšlí (push: false), migrace se commitují. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,523 @@
|
||||
# Návrh webové platformy — Martin Reiner, zbraně a střelivo
|
||||
|
||||
> Pracovní návrh vývojové a produkční platformy. Stav: před scaffoldem.
|
||||
> Datum: 2026-07-30
|
||||
> Revize: databáze změněna z PostgreSQL na SQLite po upřesnění rozsahu (viz [2. Volba stacku](#2-volba-stacku)).
|
||||
> Revize: deploy přepsán na Gitea Actions + Gitea registry a sjednocen se zavedeným postupem z [Dashboardu](../Dashboard/) — pojmenování repa, rozdělení compose souborů i podoba CI (viz [4.](#4-pojmenování-repozitáře) a [7.](#7-deploy)).
|
||||
> **Stav: naskafoldováno.** Projekt existuje, lint/testy/build i produkční image ověřeny. Produkční větev je `release` (výchozí větev repa na Giteji), ne `main`.
|
||||
|
||||
## Obsah
|
||||
|
||||
- [1. Zadání](#1-zadání)
|
||||
- [2. Volba stacku](#2-volba-stacku)
|
||||
- [3. Topologie](#3-topologie)
|
||||
- [4. Pojmenování repozitáře](#4-pojmenování-repozitáře)
|
||||
- [5. Struktura repa](#5-struktura-repa)
|
||||
- [6. Konfigurační soubory](#6-konfigurační-soubory)
|
||||
- [7. Deploy](#7-deploy)
|
||||
- [8. Zálohy](#8-zálohy)
|
||||
- [9. Cesta dál](#9-cesta-dál)
|
||||
- [10. Otevřené otázky](#10-otevřené-otázky)
|
||||
|
||||
---
|
||||
|
||||
## 1. Zadání
|
||||
|
||||
| Parametr | Hodnota |
|
||||
|---|---|
|
||||
| Typ webu | Prezentace + jednoduchá administrace obsahu (CMS) |
|
||||
| Obsah | Položky zboží: obrázek/obrázky, pár parametrů, cena. Nic víc. |
|
||||
| Administrace | Přihlášení heslem přímo na webu, přidat / upravit / odebrat položku. Žádné zákaznické účty, žádný košík. |
|
||||
| Produkce | Stávající server s Dockerem v domácí síti (tentýž, kde běží Dashboard) |
|
||||
| HTTPS | Stávající domácí Caddy, veřejná IP, Let's Encrypt |
|
||||
| Jazyk | TypeScript / JavaScript |
|
||||
| K dispozici | Gitea (vlastní), Gitea Actions, server s Dockerem, Caddy |
|
||||
| Požadavek | Oddělené dev a prod prostředí, vývoj na notebooku, nasazení na jiném stroji |
|
||||
|
||||
---
|
||||
|
||||
## 2. Volba stacku
|
||||
|
||||
| Volba | Důvod |
|
||||
|---|---|
|
||||
| **Payload CMS 3** | Admin rozhraní i veřejný web jsou **jedna Next.js aplikace**, ne dvě služby. Schéma obsahu se píše v TypeScriptu → typy pro frontend zdarma. Odpadá oddělený headless CMS. |
|
||||
| **Next.js** | Součást Payloadu 3, není to volba navíc. |
|
||||
| **SQLite** (`@payloadcms/db-sqlite`) | Payload bez databáze neběží, ale na tenhle rozsah stačí soubor. Jeden editor, žádné souběžné zápisy, desítky až stovky položek — Postgres by tu neřešil nic, jen přidal službu, heslo, healthcheck a druhý bod selhání. |
|
||||
| **Docker Compose** | `docker-compose.yml` pro vývoj, `docker-compose.prod.yml` pro nasazení — stejné rozdělení jako u Dashboardu. Prostředí (prod / staging) se liší jen `.env`. |
|
||||
| **Gitea Actions** | CI už je k dispozici, takže image staví runner, ne produkční server. Server pak jen `docker compose pull` — deploy trvá sekundy místo minut a produkce nikdy nekompiluje. |
|
||||
| **Gitea container registry** | Vestavěná součást Gitey, netřeba Harbor ani GHCR. Běží za stejným Caddy, takže má platný certifikát a odpadá otravování s `insecure-registries`. |
|
||||
|
||||
Zvažované a zamítnuté varianty:
|
||||
|
||||
- **PostgreSQL** — v první verzi návrhu. Zamítnut po upřesnění rozsahu: pro obrázek, pár parametrů a cenu je to služba navíc bez přínosu. Payload adaptér se mění za jeden řádek, takže případný pozdější přechod je export/import pár set řádků, ne přestavba.
|
||||
- **Astro + Decap CMS** — jednodušší provoz (obsah v gitu, žádná DB), ale slabší admin UX, nutný GitHub OAuth a rebuild po každé editaci. Horší cesta k budoucímu e-shopu.
|
||||
- **Astro + soubory v gitu** — nejjednodušší provoz, ale položky by musel přidávat vývojář, ne majitel. Neplní zadání.
|
||||
- **Astro + Directus** — dvě samostatné služby místo jedné, víc pohyblivých dílů bez odpovídajícího přínosu.
|
||||
|
||||
---
|
||||
|
||||
## 3. Topologie
|
||||
|
||||
```
|
||||
┌─ notebook (vývoj) ─────┐
|
||||
│ pnpm dev → :3000 │
|
||||
│ DB = soubor v ./data │
|
||||
└───────────┬────────────┘
|
||||
│ git push
|
||||
▼
|
||||
┌─ gitea.doubynet.eu ────────────────────────┐
|
||||
│ repo release → produkce, devel → staging│
|
||||
│ Actions: lint → test → build image │
|
||||
│ registry: honza/zbrane-reiner-web:<sha> │
|
||||
└───────────┬────────────────────────────────┘
|
||||
│ docker compose pull (server si sáhne pro image)
|
||||
▼
|
||||
┌─ server s Dockerem ────────┐
|
||||
│ prod: app :3000 │
|
||||
│ staging: app :3001 │
|
||||
│ data + media volumes │
|
||||
└───────────▲────────────────┘
|
||||
│ reverse_proxy
|
||||
┌───────────┴─────────────────┐
|
||||
│ Caddy │
|
||||
│ reiner-zbrane.cz → 3000│
|
||||
│ dev.reiner-zbrane.cz → 3001│
|
||||
│ gitea.doubynet.eu → Gitea│
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
Prod i staging běží na **jednom serveru** jako dva izolované Compose projekty — jiný `name:`, jiné porty, jiná volumes, jiný databázový soubor. Stačí stroj, který už máte, a na něm dva kontejnery navíc.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pojmenování repozitáře
|
||||
|
||||
### Rozhodnutí: zůstat u `Zbrane-Reiner-Web`
|
||||
|
||||
První verze tohoto dokumentu navrhovala `web-reiner-zbrane` (malá písmena, prefix podle typu). **Zamítnuto** — v `~/Dokumenty/Repo/` už je zavedená jiná konvence a ta má přednost před teorií:
|
||||
|
||||
```
|
||||
Curator Dashboard Dokumentace GOGUpdater
|
||||
PlanetaryTime RWCalc SQLmem Trainyard Zbrane-Reiner-Web
|
||||
```
|
||||
|
||||
TitleCase, žádný prefix podle typu, název = co to je. Zavést pro jeden projekt jiné pravidlo je horší než jakákoli nedokonalost toho stávajícího — konvence má cenu jen když platí všude.
|
||||
|
||||
**Velká písmena Dockeru nevadí**, protože je CI srovná — přesně jak to dělá Dashboard:
|
||||
|
||||
```yaml
|
||||
echo "IMAGE=gitea.doubynet.eu/$(echo '${{ github.repository }}' \
|
||||
| tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
```
|
||||
|
||||
`Honza/Zbrane-Reiner-Web` → `honza/zbrane-reiner-web`. Bez ručního zásahu.
|
||||
|
||||
### Propis do konfigurace
|
||||
|
||||
```
|
||||
Gitea repo gitea.doubynet.eu/Honza/Zbrane-Reiner-Web (private)
|
||||
adresář ~/Dokumenty/Repo/Zbrane-Reiner-Web
|
||||
větve release (produkce), devel (staging)
|
||||
|
||||
doména reiner-zbrane.cz → Caddy → server:3000
|
||||
staging dev.reiner-zbrane.cz → Caddy → server:3001
|
||||
|
||||
na serveru /srv/reiner-zbrane (release)
|
||||
/srv/reiner-zbrane-staging (devel)
|
||||
|
||||
compose name reiner-zbrane / reiner-zbrane-staging
|
||||
volumes reiner-zbrane_data, reiner-zbrane_media
|
||||
databáze /app/data/reiner.db (uvnitř volume data)
|
||||
image gitea.doubynet.eu/honza/zbrane-reiner-web:<tag>
|
||||
```
|
||||
|
||||
Jméno repa (`Zbrane-Reiner-Web`) a jméno nasazení (`reiner-zbrane`) se **záměrně liší**. Repo je katalogová položka, nasazení je runtime — sufix `-Web` v cestách na serveru a ve jménech volumes nenese žádnou informaci, tam je webem všechno.
|
||||
|
||||
### Doplňky k pojmenování
|
||||
|
||||
- **Repo držet private** — skončí v něm struktura `.env`, migrace a časem integrace na dodavatele.
|
||||
- **Přejmenovat před nasazením, ne po něm.** Název se propisuje do jmen Docker volumes; změna po nasazení znamená ruční migraci dat.
|
||||
- **Přibude-li druhý projekt téhož klienta**, vznikne `Zbrane-Reiner-Strelnice` — stejný vzor, řadí se vedle sebe.
|
||||
- **Monorepo** — pokud by víc webů sdílelo komponenty nebo design, ne N repozitářů, ale jedno repo s `apps/*` a `packages/ui`. Tento návrh do monorepa přejde bez přestavby.
|
||||
|
||||
---
|
||||
|
||||
## 5. Struktura repa
|
||||
|
||||
```
|
||||
Zbrane-Reiner-Web/
|
||||
├─ src/
|
||||
│ ├─ collections/ # Products.ts, Media.ts, Pages.ts, Users.ts ← schéma obsahu
|
||||
│ ├─ app/(frontend)/ # veřejný web
|
||||
│ │ ├─ page.tsx # výpis nabídky
|
||||
│ │ ├─ nabidka/[slug]/ # detail položky
|
||||
│ │ ├─ [slug]/ # statické stránky z kolekce Pages
|
||||
│ │ └─ api/health/ # endpoint pro Docker HEALTHCHECK
|
||||
│ ├─ app/(payload)/ # admin, generuje Payload
|
||||
│ ├─ lib/format.ts # formátování ceny a stavu
|
||||
│ └─ payload.config.ts
|
||||
├─ migrations/ # verzované DB migrace, commitují se
|
||||
├─ .gitea/workflows/ci.yml # lint → test → build → push do registry
|
||||
├─ Dockerfile
|
||||
├─ .dockerignore
|
||||
├─ docker-compose.yml # lokální vývoj (build z Dockerfile)
|
||||
├─ docker-compose.prod.yml # produkce (image z registry)
|
||||
├─ data/ # databázový soubor, NIKDY v gitu
|
||||
├─ .env.example # v gitu
|
||||
├─ .env # NIKDY v gitu
|
||||
├─ PROJECT.md # cíle a fáze projektu
|
||||
├─ CHANGELOG.md
|
||||
└─ README.md
|
||||
```
|
||||
|
||||
Rozdělení na `docker-compose.yml` a `docker-compose.prod.yml` (místo jednoho souboru s overridy) je převzaté z [Dashboardu](../Dashboard/) — prod soubor je samostatný, čitelný a na serveru je jediné, co tam z repa musí být.
|
||||
|
||||
### Obsahový model a přístup
|
||||
|
||||
| Kolekce | Obsah |
|
||||
|---|---|
|
||||
| `Products` | název, obrázky (vazba na Media), parametry (ráže, délka hlavně, stav…), cena, popis, příznak „prodáno" |
|
||||
| `Media` | nahrané obrázky, Payload k nim sám generuje zmenšeniny |
|
||||
| `Pages` | statické stránky — o nás, kontakt, otevírací doba |
|
||||
| `Users` | **jeden účet pro majitele.** Registrace vypnutá, účet se založí jednou při prvním spuštění. |
|
||||
|
||||
Administrace je na `/admin` — přihlášení e-mailem a heslem, za ním rovnou seznam položek s tlačítky přidat / upravit / smazat. Žádná další role, žádné zákaznické účty. Veřejná část položky jen vypisuje.
|
||||
|
||||
> **Přihlašování ven z internetu.** Admin je dostupný na veřejné doméně, takže: silné heslo, zapnout Payloadu `maxLoginAttempts` + `lockTime` a zvážit two-factor. Alternativa, pokud stačí správa z domácí sítě — pustit `/admin` v Caddy jen z LAN.
|
||||
|
||||
---
|
||||
|
||||
## 6. Konfigurační soubory
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
Záměrně **bez** Next `standalone` outputu: image je o pár set MB větší, ale Payload CLI (migrace) v něm funguje bez ladění file-tracingu. Na tento objem provozu je to správný kompromis.
|
||||
|
||||
Skutečná podoba je v [Dockerfile](Dockerfile); podstatné jsou tři věci, které nejsou zřejmé:
|
||||
|
||||
```dockerfile
|
||||
# `corepack enable` jen vytvoří shim; bez `prepare` by si kontejner stahoval
|
||||
# pnpm z npmjs.com až při startu — a bez internetu by vůbec nenaběhl.
|
||||
RUN corepack enable && corepack prepare pnpm@11.18.0 --activate
|
||||
|
||||
# Placeholder inline, ne přes ENV — jinak zůstane secret ve vrstvě image
|
||||
# (a `docker build` na to právem nadává).
|
||||
RUN PAYLOAD_SECRET=build-time-placeholder-nahrazen-za-behu pnpm build
|
||||
|
||||
# sqlite jen kvůli zálohám (`.dump`), ~1,5 MB
|
||||
RUN apk add --no-cache sqlite
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
|
||||
|
||||
# Migrace proběhnou při startu, teprve pak naskočí server.
|
||||
CMD ["sh", "-c", "pnpm payload migrate && pnpm start"]
|
||||
```
|
||||
|
||||
`HEALTHCHECK` je převzatý ze vzoru [Dashboardu](../Dashboard/Dockerfile) — `docker ps` pak rovnou ukazuje, jestli aplikace opravdu odpovídá, ne jen jestli proces běží. Míří na [`/api/health`](src/app/(frontend)/api/health/route.ts), který **záměrně nesahá do databáze**: kdyby ji kontroloval, výpadek databáze by Docker „léčil“ restartem aplikace, což nepomůže.
|
||||
|
||||
Ověřeno: image naběhne i s `--network none`, tedy bez internetu.
|
||||
|
||||
### `docker-compose.yml` (lokální vývoj)
|
||||
|
||||
```yaml
|
||||
# Lokální vývoj: build z Dockerfile. Databáze je soubor v ./data/.
|
||||
name: reiner-zbrane-dev
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./src:/app/src # změny bez rebuildu
|
||||
- ./data:/app/data
|
||||
- ./media:/app/media
|
||||
command: pnpm dev
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Na notebooku se ale běžně **ani tohle nepoužije** — Payload se pouští nativně přes `pnpm dev` kvůli rychlému hot-reloadu. Tenhle soubor slouží k ověření, že se obraz vůbec postaví a rozjede.
|
||||
|
||||
### `docker-compose.prod.yml` (produkce i staging)
|
||||
|
||||
```yaml
|
||||
# Produkční nasazení: image se STAHUJE z Gitea registry (nestaví se lokálně).
|
||||
# Na serveru (jednorázově): docker login gitea.doubynet.eu
|
||||
# Nasazení / aktualizace:
|
||||
# docker compose -f docker-compose.prod.yml pull
|
||||
# docker compose -f docker-compose.prod.yml up -d
|
||||
name: ${PROJECT:-reiner-zbrane}
|
||||
|
||||
services:
|
||||
app:
|
||||
image: gitea.doubynet.eu/honza/zbrane-reiner-web:${IMAGE_TAG:-latest}
|
||||
ports:
|
||||
- "${APP_PORT:-3000}:3000" # viz poznámka o Caddy níže
|
||||
env_file: .env
|
||||
volumes:
|
||||
- data:/app/data # databázový soubor
|
||||
- media:/app/media # nahrané obrázky přežijí redeploy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
```
|
||||
|
||||
Jedna služba, dva volumes, žádné heslo k databázi. Celý stav webu = tyhle dva volumes.
|
||||
|
||||
### `.env.example`
|
||||
|
||||
```dotenv
|
||||
# Vzor konfigurace. Zkopíruj do `.env` a vyplň. `.env` NIKDY necommituj.
|
||||
PROJECT=reiner-zbrane
|
||||
APP_PORT=3000
|
||||
IMAGE_TAG=latest
|
||||
|
||||
DATABASE_URI=file:./data/reiner.db
|
||||
|
||||
# Klíč pro podpis session cookie – v produkci náhodný řetězec min. 32 znaků!
|
||||
PAYLOAD_SECRET=zmen-me-na-nahodny-retezec
|
||||
NEXT_PUBLIC_SERVER_URL=https://reiner-zbrane.cz
|
||||
```
|
||||
|
||||
Staging má `PROJECT=reiner-zbrane-staging`, `APP_PORT=3001`, `IMAGE_TAG=devel`, vlastní `PAYLOAD_SECRET` a `NEXT_PUBLIC_SERVER_URL=https://dev.reiner-zbrane.cz`. `DATABASE_URI` zůstává stejné — soubor je v jiném volume, takže se nemají jak potkat.
|
||||
|
||||
`IMAGE_TAG` je zároveň páka na rollback: přepsat na předchozí SHA, `pull`, `up -d`.
|
||||
|
||||
### Caddyfile (na stávajícím domácím Caddy)
|
||||
|
||||
```caddyfile
|
||||
reiner-zbrane.cz, www.reiner-zbrane.cz {
|
||||
reverse_proxy 192.168.1.50:3000
|
||||
}
|
||||
|
||||
dev.reiner-zbrane.cz {
|
||||
basic_auth { honza <bcrypt-hash> } # staging schovaný před světem i roboty
|
||||
reverse_proxy 192.168.1.50:3001
|
||||
}
|
||||
```
|
||||
|
||||
Hash hesla: `caddy hash-password`.
|
||||
|
||||
Caddy už tímhle způsobem obsluhuje Gitea i Dashboard, takže přibývají jen dva bloky.
|
||||
|
||||
> **Síťová poznámka.** Pokud Caddy běží na **jiném stroji** než Docker, musí porty poslouchat na LAN, ne na `127.0.0.1`, a přístup se omezí firewallem:
|
||||
>
|
||||
> ```bash
|
||||
> ufw allow from <IP-Caddy> to any port 3000,3001 proto tcp
|
||||
> ufw default deny incoming
|
||||
> ```
|
||||
>
|
||||
> Pokud Caddy běží na **tomtéž stroji** (jak to vypadá u Dashboardu), je to jednodušší a bezpečnější — v `docker-compose.prod.yml` stačí publikovat port jen na loopback: `"127.0.0.1:${APP_PORT}:3000"`. Ven se pak nedostane nic mimo Caddy a žádná `ufw` pravidla netřeba.
|
||||
|
||||
---
|
||||
|
||||
## 7. Deploy
|
||||
|
||||
Postup je záměrně **stejný jako u [Dashboardu](../Dashboard/)** — CI postaví a nahraje image, server si ho stáhne. Žádný nový vzor k naučení.
|
||||
|
||||
### Pipeline
|
||||
|
||||
| Krok | Kde běží |
|
||||
|---|---|
|
||||
| 1. `git push` do `devel` / `release` | notebook |
|
||||
| 2. lint + typy + testy | Gitea Actions runner |
|
||||
| 3. build image, tag `<sha>` + `devel`/`latest`, push do registry | Gitea Actions runner |
|
||||
| 4. `docker compose pull && up -d`, migrace v entrypointu | server (ručně) |
|
||||
| 5. Caddy posílá provoz beze změny | Caddy |
|
||||
|
||||
### `.gitea/workflows/ci.yml`
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
# Spustí se při pushi do devel/release a jde i ručně spustit z GUI.
|
||||
on:
|
||||
push:
|
||||
branches: [devel, release]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Instalace pnpm
|
||||
run: corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
- name: Instalace závislostí
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: ESLint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typová kontrola
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
- name: Build (ověření, že projde)
|
||||
run: pnpm build
|
||||
|
||||
build-and-push:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Docker jméno musí být malými písmeny (Honza/Zbrane-Reiner-Web -> honza/zbrane-reiner-web)
|
||||
- name: Název image
|
||||
run: |
|
||||
echo "IMAGE=gitea.doubynet.eu/$(echo '${{ github.repository }}' \
|
||||
| tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Značka podle větve
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "release" ]; then
|
||||
echo "MOVING_TAG=latest" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "MOVING_TAG=devel" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Přihlášení do Gitea registry
|
||||
run: |
|
||||
echo '${{ secrets.REGISTRY_TOKEN }}' \
|
||||
| docker login gitea.doubynet.eu -u '${{ github.actor }}' --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t "$IMAGE:$MOVING_TAG" -t "$IMAGE:${{ github.sha }}" .
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push "$IMAGE:$MOVING_TAG"
|
||||
docker push "$IMAGE:${{ github.sha }}"
|
||||
```
|
||||
|
||||
Oproti Dashboardu přibyl jen **plovoucí tag podle větve** — `release` → `latest`, `devel` → `devel`. Bez toho by staging a produkce ukazovaly na tentýž `latest` a nešly by oddělit.
|
||||
|
||||
Záměrně **bez** `docker/build-push-action` — Gitea si actions tahá z github.com, takže každá závislost navíc je další věc, která se může rozbít. Holé `docker` funguje vždycky. Stejný důvod, proč to tak má Dashboard.
|
||||
|
||||
### Nasazení na serveru
|
||||
|
||||
```bash
|
||||
cd /srv/reiner-zbrane-staging # nebo /srv/reiner-zbrane
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
Migrace se pustí samy v entrypointu kontejneru. Server **nic nekompiluje** a nemá zdrojové kódy — jen `docker-compose.prod.yml`, `.env` a dva volumes.
|
||||
|
||||
### Co je potřeba nastavit jednou
|
||||
|
||||
**Na Giteji** — repo → Settings → Secrets: `REGISTRY_TOKEN` (osobní token s oprávněním `write:package`). Stejný secret, jaký už používá Dashboard.
|
||||
|
||||
**Runner** — `act_runner` už běží kvůli Dashboardu, další projekt nepotřebuje nic navíc.
|
||||
|
||||
**Na serveru** — jednorázově:
|
||||
|
||||
```bash
|
||||
mkdir -p /srv/reiner-zbrane /srv/reiner-zbrane-staging
|
||||
# do každého: docker-compose.prod.yml + .env
|
||||
docker login gitea.doubynet.eu # aby šlo image stáhnout
|
||||
```
|
||||
|
||||
### Dva adresáře na serveru
|
||||
|
||||
| Cesta | Větev | Tag | Port | Doména |
|
||||
|---|---|---|---|---|
|
||||
| `/srv/reiner-zbrane` | `release` | `latest` | 3000 | `reiner-zbrane.cz` |
|
||||
| `/srv/reiner-zbrane-staging` | `devel` | `devel` | 3001 | `dev.reiner-zbrane.cz` |
|
||||
|
||||
Každý má vlastní `.env`. Liší se `PROJECT`, `APP_PORT`, `IMAGE_TAG` a `PAYLOAD_SECRET`.
|
||||
|
||||
### Denní workflow
|
||||
|
||||
```bash
|
||||
# vývoj
|
||||
git switch -c feature/kontaktni-formular
|
||||
pnpm dev # localhost:3000
|
||||
|
||||
# změna schématu obsahu → migrace do gitu
|
||||
pnpm payload migrate:create pridani-formulare
|
||||
git add . && git commit && git push
|
||||
|
||||
# staging
|
||||
git switch devel && git merge feature/kontaktni-formular && git push
|
||||
ssh server 'cd /srv/reiner-zbrane-staging && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d'
|
||||
|
||||
# produkce, až to na dev.reiner-zbrane.cz sedí
|
||||
git switch release && git merge devel && git push
|
||||
ssh server 'cd /srv/reiner-zbrane && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d'
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
sed -i 's/^IMAGE_TAG=.*/IMAGE_TAG=<predchozi-sha>/' .env
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
Funguje, protože každý build je otagovaný svým SHA a v registry zůstává. Migrace jsou ale jednosměrné — pokud verze mezitím změnila schéma databáze, patří k rollbacku i obnova zálohy.
|
||||
|
||||
---
|
||||
|
||||
## 8. Zálohy
|
||||
|
||||
S CMS je databáze **jediná kopie obsahu** — na rozdíl od statického webu ji nemáte v gitu. Nepodceňovat.
|
||||
|
||||
Cron na serveru, denně:
|
||||
|
||||
```bash
|
||||
cd /srv/reiner-zbrane
|
||||
docker compose -f docker-compose.prod.yml exec -T app \
|
||||
sqlite3 /app/data/reiner.db .dump | gzip > /backup/db-$(date +%F).sql.gz
|
||||
docker run --rm -v reiner-zbrane_media:/m -v /backup:/b alpine \
|
||||
tar czf /b/media-$(date +%F).tgz -C /m .
|
||||
find /backup -mtime +14 -delete
|
||||
```
|
||||
|
||||
`.dump` běží v transakci, takže je konzistentní i za provozu — soubor **nekopírovat** obyčejným `cp`, ten za běhu s WAL zachytí rozpracovaný zápis. Obnova: `zcat db-2026-07-30.sql.gz | sqlite3 reiner.db`.
|
||||
|
||||
Zálohy odvézt i mimo server (rsync/restic na NAS nebo do cloudu) — záloha na stejném stroji neřeší selhání disku.
|
||||
|
||||
**Obnovu jednou za čas opravdu vyzkoušet na stagingu.** Záloha, kterou jste nikdy nenačetli, není záloha.
|
||||
|
||||
---
|
||||
|
||||
## 9. Cesta dál
|
||||
|
||||
Nic z toho neřešit teď — ruční `pull && up -d` vydrží hodně dlouho.
|
||||
|
||||
1. **Automatický deploy stagingu** — buď [Watchtower](https://containrrr.dev/watchtower/) sledující tag `devel`, nebo krok navíc v CI, který se přes SSH přihlásí na server a spustí `pull && up -d`. Ušetří jeden příkaz po každém pushi do `devel`; u produkce je ruční krok spíš výhoda.
|
||||
2. **Buildx cache v CI** — první build instaluje závislosti od nuly. S `--cache-from` proti registry se sestavení zkrátí na desítky sekund.
|
||||
3. **E-shop** — Payload má oficiální plugin pro platby; přechod nevyžaduje změnu infrastruktury.
|
||||
4. **Postgres**, ale až kdyby přišly souběžné zápisy (objednávky, víc editorů současně). Mění se jeden adaptér v `payload.config.ts` a přidává jedna služba v `docker-compose.prod.yml`; data se převedou exportem a importem. Do té doby by to byla údržba navíc bez užitku.
|
||||
|
||||
---
|
||||
|
||||
## 10. Otevřené otázky
|
||||
|
||||
- [ ] **Příjmení: „Reirenr" vs. „Reiner"** — vypadá to jako překlep. Celý dokument počítá s `reiner`. Potvrdit před scaffoldem, protože se to propíše do jmen Docker volumes a přejmenování po nasazení znamená ruční migraci dat.
|
||||
- [ ] **Skutečná doména** — do `NEXT_PUBLIC_SERVER_URL` a Caddyfile. Název repa už na ní nezávisí, ten je daný konvencí.
|
||||
- [ ] **Běží Caddy na tomtéž stroji jako Docker?** — pokud ano, publikovat porty jen na `127.0.0.1` a vynechat `ufw` pravidla. Pokud ne, doplnit IP serveru (v návrhu placeholder `192.168.1.50`).
|
||||
- [ ] **Porty 3000/3001 volné?** — na serveru už běží Dashboard na `:8000`, kolize nehrozí, ale ověřit.
|
||||
- [ ] **Smazat prázdný adresář `Web-ZaS`.**
|
||||
|
||||
---
|
||||
|
||||
## Další krok
|
||||
|
||||
Po potvrzení otevřených otázek:
|
||||
|
||||
1. `git init` v tomto repu, remote na `gitea.doubynet.eu/Honza/Zbrane-Reiner-Web`, větve `release` + `devel`
|
||||
2. vygenerovat Payload 3 + Next.js projekt se SQLite adaptérem
|
||||
3. napsat `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `docker-compose.prod.yml`, `.env.example`, `.gitea/workflows/ci.yml`, `README.md`, `PROJECT.md`, `CHANGELOG.md`
|
||||
4. přidat kolekce `Products`, `Media`, `Pages`, `Users`, vypnout veřejnou registraci a založit jediný účet pro majitele
|
||||
5. postavit veřejný výpis položek a detail položky
|
||||
Reference in New Issue
Block a user