Scaffold webu na Payload CMS 3 + Next.js nad SQLite
CI / test (push) Failing after 1m42s
CI / build-and-push (push) Has been skipped

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:
2026-07-30 13:13:23 +02:00
co-authored by Claude Opus 5
parent fedccbee93
commit c8e435139e
49 changed files with 13152 additions and 49 deletions
+523
View File
@@ -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