Files
ems/docs/deployment-self-hosted.md
Dusan Vojacek 5bfea4457b
All checks were successful
CI and deploy / migration-check (push) Successful in 37s
CI and deploy / deploy (push) Has been skipped
docs+re-trigger: CI gotcha prazdny commit/duplicitni SHA netriggeruje deploy; re-trigger Faze 0 (V106)
Faze 0 (battery guard + EV reg15/session, V106) zustala na serveru nenasazena
(V105) — prazdny re-trigger commit se neprojevil. Tento neprazdny commit na main
(unikatni SHA, ref=main) spusti realny deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:20:35 +02:00

216 lines
11 KiB
Markdown
Raw 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.
# Self-hosted deploy: Gitea + Caddy + EMS (`/opt/ems-deploy`)
Runbook pro **single-node Debian**, **Docker Compose**, **Gitea Actions** bez Kubernetes. Doplňuje konkrétní stav serveru (ZFS, `zfs` storage driver, Caddy na hostu, Gitea v `/opt/gitea-stack`).
---
## 1. Co kde běží
| Oblast | Cesta / adresa | Poznámka |
|--------|----------------|----------|
| Gitea (HTTP) | `https://git.vojacek.eu` → Caddy → `127.0.0.1:3000` | Gitea publikuje HTTP jen na loopback. |
| Gitea (Git SSH) | `git.vojacek.eu:2222` → kontejner `:22` | Host SSH zůstává na `:22`. V `app.ini`: `START_SSH_SERVER = false` (kvůli SSH v base image). |
| Gitea stack | `/opt/gitea-stack` | `postgres`, `gitea`, `gitea-runner`; vlastní `docker-compose` + `.env`. |
| Gitea runner | stejný stack | `act_runner` s přepsaným `entrypoint`/`command`; registrace ručně → `runner/runner.json`. |
| EMS deploy | `/opt/ems-deploy` | Oddělený Compose projekt od Gitea. |
| EMS checkout | `/opt/ems-deploy/app` | Git clone stejného repa jako v Gitea. |
| EMS Compose (runtime) | `/opt/ems-deploy/docker-compose.yml` | Kopíruje se ze `app/deploy/docker-compose.yml` při každém deployi (`deploy.sh`). |
| EMS secrets | `/opt/ems-deploy/.env` | **Není v gitu**; vzor `.env.example` v repu. |
**PostgREST v produkčním `deploy/docker-compose.yml`:** služba naslouchá v **Docker síti** na `:3000`, **bez** mapování na host — frontend v kontejneru volá `postgrest:3000`. Tím se vyhne kolizi s Gitea na hostovém `127.0.0.1:3000`. Pokud někdy přidáš `ports` pro ladění, použij jiný host port než 3000.
---
## 2. Architektonický verdikt
**Směr (push → Actions → skript na hostu → `docker compose build && up`) je správný** pro cíl „jeden server, žádný registry, žádný DinD“.
**Tvrdá fakta / rizika**
1. **Job Gitea Actions neběží na hostu**, ale ve **job kontejneru** (Docker executor). Kroky typu `run: /opt/ems-deploy/deploy.sh` tedy **neuvidí** hostovské cesty, dokud je nepřimountuješ do job kontejneru a nepřidáš je do `valid_volumes` u runneru.
2. **Mount Docker socketu do jobu = plná moc nad hostovským Dockerem** (ekvivalent root pro kontejnery). Je to běžný pattern u self-hosted runnerů; bezpečnost = důvěra k přístupu do repa a k labelům runneru, ne veřejný runner.
3. **`docker: command not found` v jobu** je očekávané u defaultního image — buď použij image s `docker` + `docker compose`, nebo vůbec nevolaj docker uvnitř jobu (u tebe stačí spustit `deploy.sh`, který mluví s hostovským daemonem přes socket).
4. **ZFS + Docker `zfs` driver:** snapshoty a `zfs list` rostou s image vrstvami; `deploy.sh` už dělá `docker image prune -f` (dangling). Hlídej místo na poolu a případně periodický `docker system prune` (opatrně — nesmí mazat věci, co potřebuješ).
5. **`git reset --hard origin/main`:** rychlé a deterministické; jakýkoli lokální drift v `/opt/ems-deploy/app` na serveru se **ztratí**. Na produkční app adresář nesahat ručně — vždy přes git + deploy.
6. **Dvě síťě:** `ems_net` vs `gitea_net` — služby se defaultně nevidí; pro EMS to obvykle nevadí. Integrovat Gitea DB s EMS nechceš.
---
## 3. Tok deploye (cílový)
```mermaid
flowchart LR
dev[Dev laptop] -->|git push| gitea[Gitea main]
gitea --> act[Gitea Actions]
act --> runner[act_runner + Docker]
runner --> ci[migration-check validate]
ci -->|needs ok| job[deploy job + /opt mount]
job --> sh[deploy.sh]
sh --> git[git fetch / reset main]
sh --> sync[install compose.yml]
sh --> fly[flyway validate then migrate]
sh --> dc["docker compose build && up -d"]
dc --> hostd[Host Docker Engine]
hostd --> ems[EMS stack]
```
1. Vývoj lokálně, merge do `main`, push na `git.vojacek.eu:2222`.
2. Workflow `.gitea/workflows/deploy.yml` (label `self-hosted`) spustí job.
3. Job kontejner má `docker.sock` a rw mount `/opt/ems-deploy`; nainstaluje `git`, `bash`, `docker-cli`, `docker-cli-compose` (Alpine).
4. `/opt/ems-deploy/deploy.sh`: `flock`, `git` v `app/`, sync `docker-compose.yml`, `docker compose config`, **`flyway validate`** (soubory vs `flyway_schema_history` na DB), **`flyway migrate`**, `build`, `up -d`, `image prune -f`.
Build probíhá na **hostovském** Dockeru (stejný daemon jako Gitea stack), bez DinD.
---
## 4. Bootstrap serveru (jednorázově)
Přesně podle hlavičky v `deploy/deploy.sh`:
```bash
sudo mkdir -p /opt/ems-deploy/app
sudo chown -R "$USER:$USER" /opt/ems-deploy
git clone ssh://git@git.vojacek.eu:2222/vojacekd/ems.git /opt/ems-deploy/app
cp /opt/ems-deploy/app/.env.example /opt/ems-deploy/.env
chmod 600 /opt/ems-deploy/.env
# doplnit secrets / DB / JWT / …
install -m 755 /opt/ems-deploy/app/deploy/deploy.sh /opt/ems-deploy/deploy.sh
/opt/ems-deploy/deploy.sh
```
Uživatel, pod kterým běží runnerův job (root v Alpine job image), musí mít právo číst `.env`, psát do `app/.git` a volat Docker. Typicky runner běží jako root → ověř oprávnění na `/opt/ems-deploy`.
---
## 5. Gitea runner — nutná úprava `config.yaml`
Aby job směl přimountovat hostové cesty, v `runner/config.yaml` (na serveru v `/opt/gitea-stack/runner/config.yaml`) **musí** být v `container.valid_volumes` povolené alespoň:
```yaml
container:
network: host
privileged: false
valid_volumes:
- /opt/ems-deploy
- /var/run/docker.sock
```
Bez toho Actions mounty odmítnou / job spadne. Po změně restart runner kontejneru.
`labels` při registraci runneru musí odpovídat `runs-on` ve workflow (např. výchozí `self-hosted`).
---
## 6. Caddy a EMS (až bude veřejná doména)
Gitea blok v Caddyfile zůstává. Pro EMS přidej **samostatný** site blok, např.:
```caddy
ems.vojacek.eu {
encode gzip
@api path /rest*
handle @api {
uri strip_prefix /rest
reverse_proxy 127.0.0.1:8080
}
handle {
reverse_proxy 127.0.0.1:8080
}
}
```
**Upřesnění podle skutečného nginx ve frontend image:** pokud API jde přes stejný origin a path `/rest`, výše může stačit jeden `reverse_proxy` na `127.0.0.1:8080` bez stripu — ověř v `frontend` konfiguraci. Pro PostgREST OpenAPI nastav v `.env` / `PGRST_OPENAPI_SERVER_PROXY_URI` veřejnou bázi URL.
Produkční compose mapuje frontend na **`127.0.0.1:8080`**, backend na **`127.0.0.1:8000`**.
---
## 7. CI a deploy (jeden workflow)
| Soubor | Účel |
|--------|------|
| `.gitea/workflows/deploy.yml` | **`migration-check`** na `pull_request` a `push` (`main`, `feature/**`): checkout přes `git fetch` (bez Node), skript **neměnnosti** `scripts/ci_check_migration_immutability.sh` (proti `origin/main` nebo base SHA u PR), **`flyway validate`** proti DB z JDBC (viz secrets). **`deploy`** jen při `push` na `main`, **`needs: migration-check`** — pak `/opt/ems-deploy/deploy.sh`. |
**Gitea secrets (repo):**
| Secret | Účel |
|--------|------|
| `EMS_CI_FLYWAY_URL` | JDBC na staging / sdílenou DB s aktuální `flyway_schema_history` (např. `jdbc:postgresql://host:5432/ems`). Pokud **není** nastaveno, krok remote validate se **přeskočí** (varování v logu); immutability check pořád běží. |
| `EMS_CI_FLYWAY_USER` / `EMS_CI_FLYWAY_PASSWORD` | Volitelně, pokud nejsou v URL. |
Job **nemá** `container:` — potřebuje hostovský `docker` + `git` (stejně jako deploy). Runner: `valid_volumes` pro `/var/run/docker.sock` (sekce 5).
**Lokálně před commitem:** z kořene repa s rozjetým Postgres z `docker-compose.yml` a aspoň jednou proběhlým migrate spusť `./scripts/flyway_validate_local.sh` (`flyway validate` vůči lokální DB).
**Flyway `validate` vs `migrate`:** `validate` nekontroluje „dry-run“ celého SQL nové pending migrace; ověří hlavně shodu **již zapsaných** řádků ve `flyway_schema_history` se soubory v repu (checksumy atd.) — proto musí běžet proti **běžící** DB. `migrate` teprve aplikuje pending skripty.
`workflow_dispatch` na větvi `main` spustí `migration-check` a potom i **`deploy`** (stejná podmínka jako u push na `main`).
---
## 8. Co záměrně neděláme (zatím)
- Privátní Docker registry na stejném stroji.
- Buildkit cache export / remote cache.
- k3s/Kubernetes.
- Spouštění `docker compose` uvnitř jobu bez přístupu k **jednomu** hostovskému daemonu (tj. bez soku bys musel do DinD nebo remote docker).
---
## 9. Rychlá kontrola po deployi
```bash
docker compose -f /opt/ems-deploy/docker-compose.yml --env-file /opt/ems-deploy/.env ps
curl -sf http://127.0.0.1:8000/docs >/dev/null && echo backend OK
curl -sf http://127.0.0.1:8080/ >/dev/null && echo frontend OK
```
---
## 10. Reset DB a obnova z dumpu
Kompletní postup (zastavení služeb, `down`, smazání volume `db_data`, jen `db`, `import_ems_db.sh`, Flyway, `up`): **[database-reset-and-restore.md](database-reset-and-restore.md)**.
---
## 11. Odkazy v repu
- `deploy/deploy.sh` — jediný produkční vstup „co se na serveru spouští“.
- `deploy/docker-compose.yml` — šablona runtime Compose (kopíruje se do `/opt/ems-deploy`).
- `.gitea/workflows/deploy.yml` — napojení na runner + job kontejner.
- `docs/database-reset-and-restore.md` — zahození dat Postgres/Timescale a import zálohy.
## CI/deploy opravy 2026-06-11/12 (container mód runneru, slabý server)
Tři nezávislé vady, které od 6. 6. blokovaly auto-deploy (runy 358369):
1. **Secret `EMS_CI_FLYWAY_URL`** mířil na `localhost` — DB poslouchá na
`EMS_DB_BIND` (10.200.200.1). Validate flyway běží s `--network host`.
2. **Bind mounty v container módu**: docker CLI v jobu mluví s HOSTOVSKÝM
daemonem → `-v` cesty checkoutu neexistují → flyway dostal prázdné adresáře
(„applied migration not resolved locally"). Fix: `docker create` +
`docker cp` do **`/sql`** (ne `/flyway/sql` — image tam má VOLUME, který by
kopii zastínil) — `scripts/ci_flyway_validate_remote.sh`.
3. **Pending repeatables**: flyway 12 validate selže na nové/změněné R__ —
ignorovat `*:pending` (CI skript i `deploy/deploy.sh`; immutabilitu
verzovaných hlídá `ci_check_migration_immutability.sh`).
Dále: workflow deploy step si **sám instaluje čerstvý `deploy.sh`** z checkoutu
(opravy skriptu se propagují bez ručního zásahu) a `deploy.sh` **stopne
backend/frontend/postgrest PŘED flyway** — slabý server se s běžícím stackem
+ buildem dusil tak, že flyway nedostal spojení k DB (run 369: 9,5 min → EOF).
Výpadek app vrstvy během deploye = navržená degradace (Loxone fallback,
TeltoCharge failsafe).
## CI gotcha: prázdný commit / duplicitní SHA na main+dev netriggeruje deploy (2026-06-13)
`git commit --allow-empty` ani push **stejného SHA** na main i dev hned po sobě
nespustí reálný `deploy` job (Gitea workflow se naváže na poslední ref = dev →
`if: github.ref == 'refs/heads/main'` false → skip; combined status hlásí
„success", protože skip ≠ fail — viz per-JOB kontrola). **Re-trigger deploye
vždy NEPRÁZDNÝM commitem na main, který v tu chvíli NENÍ na dev** (unikátní SHA
s ref=main). Ověření, že deploy reálně proběhl: `flyway_schema_history`
nejnovější V### (ne jen combined status success).