Files
HonzaandClaude Opus 5 c8e435139e
CI / test (push) Failing after 1m42s
CI / build-and-push (push) Has been skipped
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>
2026-07-30 13:13:23 +02:00

524 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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