Files
ems/docs/deployment-self-hosted.md
Dusan Vojacek 906eeb1609
All checks were successful
CI and deploy / migration-check (push) Successful in 3s
CI and deploy / deploy (push) Successful in 14s
flyway check
2026-04-19 14:11:57 +02:00

9.1 KiB

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ý)

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:

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ň:

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ř.:

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

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.


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.