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>
216 lines
11 KiB
Markdown
216 lines
11 KiB
Markdown
# 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 358–369):
|
||
|
||
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` má
|
||
nejnovější V### (ne jen combined status success).
|