Per-app kontejner — architektonická specifikace¶
Status: draft (rozhodnuto 2026-05-14) Verze spec: 0.1 Související:
backend-deploy.md,app-distribution.md,server-infrastructure.md
1. Rozhodnutí¶
Posun od dynamického mountu Python modulů do hlavního backendu (backend/app/dynamic_mounts.py) k izolovaným kontejnerům pro každou aplikaci. Hlavní backend zůstává centrálním auth/org/catalog/storage hubem; aplikace běží jako samostatné HTTP služby za API gateway.
Motivace:
- Izolace — pád / paměťový leak / nekonečná smyčka v jedné app nezpůsobí výpadek ostatních ani auth.
- Polyglot — apps lze psát v jiných jazycích (Go, Node, Rust), ne jen Python. Vendor si vybere stack.
- Nezávislé nasazení — deploy app vendora nevyžaduje rebuild celého backendu.
- Škálování — busy app dostane víc replik, klidná zůstává v 1 instanci.
Trade-off, který přijímáme:
- Síťová režie mezi službami (~ms latence navíc).
- Auth musí cestovat přes každý request (vyřešeno shared middleware).
- DB connections × N apps místo 1 sdíleného poolu (řešení: PgBouncer).
- Komplexnější deploy (víc kontejnerů, registry, gateway routing).
2. Současný stav (před změnou)¶
┌────────────────────────────────────┐
│ bare uvicorn :8000 (avaxdev) │
│ │
│ FastAPI app │
│ ├── /auth/* /org/* /catalog/* │
│ ├── /storage/* /admin/* │
│ └── dynamic_mounts.py: │
│ ├── /legal/* ← module │
│ │ avax_legal_backend.legal │
│ └── (další modules) │
└────────────────────────────────────┘
dynamic_mounts.pyimportuje modul Pythonavax_legal_backend.legala mountujerouter/admin_router/sync_routerdo hlavní FastAPI app- Sdílená DB session, sdílený Redis, sdílený JWT decode
- Vše v jednom procesu
Limity: 1 bad app → restart celého backendu; Python-only; sdílený paměťový prostor.
3. Cílový stav¶
┌──────────────────────────┐
│ vm-gateway (Nginx) │
│ api.avaxis.cz │
└──┬───────────────────┬───┘
│ │
│ /auth/* /org/* │ /apps/<slug>/*
│ /catalog/* │
│ /storage/* │
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ core-api │ │ apps-gateway │
│ (FastAPI) │ │ (router) │
│ :8000 │ │ :8100 │
└────────┬───────┘ └─────┬────────────┘
│ │
│ ├─► app-legal :8101
│ │ avax-legal/backend
│ │
│ ├─► app-mzdy :8102
│ │ (až bude)
│ │
│ └─► app-xxx :8103
▼
┌────────────────┐
│ PostgreSQL │ sdílená DB
│ PgBouncer │ pool sdílený přes pgbouncer
│ :5432 / :6432 │
└────────────────┘
Komponenty¶
| Komponenta | Účel | Tech |
|---|---|---|
| vm-gateway / Nginx | TLS termination, prefix routing | Nginx (existující) |
| core-api | Auth, org, catalog, storage, admin | FastAPI (= dnešní backend, mínus dynamic_mounts) |
| apps-gateway | Router /apps/<slug>/* → kontejner |
FastAPI tenké proxy NEBO Traefik label-based discovery |
app-<slug> |
Samostatný kontejner per app | Vendor volba (FastAPI/Express/Go/…) |
| PostgreSQL + PgBouncer | Sdílená DB s connection poolem | PG 16 + PgBouncer |
| Container registry | Image hosting | Gitea Container Registry (git.avaxis.cz) |
Routing pravidla¶
/auth/*,/org/*,/catalog/*,/storage/*,/admin/*→ core-api/apps/<slug>/*→ apps-gateway →app-<slug>kontejner- Cesta
<slug>se v requestu zachovává:/apps/legal/items/123→ kontejner dostane/items/123(gateway strip prefix)
4. Health endpoint — povinná konvence¶
Každá app backend povinně exposuje:
GET /health
→ 200 OK
{
"slug": "legal",
"version": "0.3.2",
"status": "ok",
"checks": {
"db": "ok",
"core_api": "ok"
}
}
- 200 = vše funkční, request je obsluhovatelný
- 503 = degraded, jeden z
checksmá"fail"(ale kontejner stále běží, jen není připraven obsluhovat) - Nikdy timeout > 5s — health musí být rychlý
- Žádná auth na
/health(gateway ho volá interně)
Apps-gateway volá /health periodicky (každých 10s). Při 503 → marker degraded (uživatelské requesty stále routovány, jen alert do monitoringu). Při 3 neúspěšných pingach → marker down, requesty vracejí 503 bez zavolání app.
Liveness vs Readiness:
- /health = readiness (může obsluhovat?)
- /health/live = liveness (proces běží?) — volitelné, default = vrací 200 dokud proces dýchá
5. Auth propagace¶
Tok JWT¶
- Klient (launcher / web) volá
/apps/legal/itemssAuthorization: Bearer <jwt> - Gateway request projde rovnou do app kontejneru (token nepáruje)
- App kontejner spustí shared middleware —
avax-authpackage - Middleware validuje JWT (HS256/RS256), naplní
request.state.users{id, company_id, system_role, email} - Endpoint v app si přečte
request.state.user
Shared middleware — avax-auth¶
Python balíček packages/avax-auth/ (v hlavním repu, instaluje se přes pip git+url). Node a Go varianty přijdou v pozdější fázi (viz Timeline).
Stav 2026-05-14: v0.1.0 alpha, 24 testů projde. Funguje proti reálným HS256 tokenům z core-api.
# main.py — jednou při startupu
from avax_auth import AvaxAuthConfig, configure
configure(AvaxAuthConfig(
algorithm="HS256", # nebo RS256 v produkci
secret=os.environ["JWT_SECRET"],
# public_key=... / jwks_url=...
))
# Endpoint
from typing import Annotated
from fastapi import Depends
from avax_auth import AvaxUser, require_user
@app.get("/items")
def list_items(user: Annotated[AvaxUser, Depends(require_user)]):
return {"items": ..., "for_user": user.id, "company": user.company_id}
Public API balíčku:
- AvaxUser — dataclass s id, company_id, system_role, tfa_verified, raw_token, claims
- require_user — FastAPI Depends, validuje Bearer token
- require_role(*roles) — factory pro role-based gating
- require_super_admin — shortcut
- validate_token(token) — low-level (bez FastAPI)
- AvaxAuthError, TokenExpired, InvalidToken, WrongTokenType, Forbidden — výjimky
JWT veřejný klíč (pro RS256) se distribuuje:
- Z JWKS endpointu GET /auth/.well-known/jwks.json ✅ implementováno 2026-05-14 — cache 1h, automatický refresh při neznámém kid
- Statický PEM přes env AVAX_JWT_PUBLIC_KEY (multi-line PEM) — alternativa pro situace bez sítě k core-api
Access/refresh tokeny jsou od 2026-05-14 podepsány RS256 s kid headerem (RFC 7638 JWK Thumbprint). HS256 fallback v decode_token() je transitional — odstranit ~7 dní po deployi (až vyprší všechny refresh tokens).
Detailní README: packages/avax-auth/README.md.
Tokeny mezi službami (machine-to-machine)¶
App backend někdy potřebuje volat core-api (např. pro GET /storage/token). Použije:
- User-on-behalf-of token — propaguje JWT uživatele, který request inicioval (preferováno)
- Service token — vystavený přes POST /auth/service-token s app slugem + secret (jen pro background jobs)
6. DB strategie¶
Per-app schema v sdílené DB¶
Doporučeno na začátek. Každá app má vlastní PostgreSQL schema (legal, mzdy, …) ve sdílené DB avaxis.
CREATE SCHEMA legal;
GRANT USAGE ON SCHEMA legal TO avax_app_legal;
GRANT ALL ON ALL TABLES IN SCHEMA legal TO avax_app_legal;
- App má vlastního DB usera
avax_app_<slug>s přístupem jen do svého schématu core-apimá useraavaxiss přístupem všude- Alembic migrace per-app,
version_table_schema=<slug>,include_schemas=True
Proč ne per-app DATABASE: - 1 PostgreSQL instance handles 100+ schemas snadno - Jedna záloha = jeden pg_dump (současný setup) - Cross-app queries (analytika, support) jsou možné
Kdy posunout na per-app DATABASE: pokud app vyžaduje custom extension nebo má tisíce tabulek.
PgBouncer¶
Power tool pro snížení connection load. Apps connect na PgBouncer (:6432), ten drží pool na real PG (:5432). Bez něj 20 apps × 10 connections = 200 connections — pro malou PG je to limit.
[databases]
avaxis = host=192.168.1.55 port=5432 dbname=avaxis_dev
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
7. S3 přístup z app¶
Nezměnen oproti dnešku — app nikdy nemá přímé S3 credentials. Vyžádá si STS token přes GET /storage/token?dir_id=<id>&scope=private v core-api. Token má 15-min platnost, scoped na konkrétní prefix.
Pro app-specifické bucky:
- app-<slug> — distribution (vendor pushuje přes avax-publish, app nečte)
- backup-<ico> — data firmy (app čte/píše přes STS token)
8. Deploy pipeline¶
Vývoj (avaxdev)¶
cd /home/avax/<slug>
docker compose up -d # spustí app na local portu
curl http://localhost:81xx/health
Apps-gateway na avaxdev má apps.yml config s mapováním <slug> → http://localhost:81xx. Update vyžaduje reload gateway (systemctl reload avax-apps-gateway).
Produkce¶
- Vendor pushne tag
v1.0.0do Gitea repa app - Gitea Actions:
- Build Docker image
- Push do
git.avaxis.cz/<vendor>/<slug>:1.0.0 - Vystaví signed manifest s SHA256 do
app-avax-<slug>/manifests/1.0.0.json - Avaxis CI:
- Pull image
- Run security scan
- Deploy do
stagingcluster (<slug>-staging) - Smoke test (health + ~5 testovacích requestů)
- Pokud OK → manual approval → promote do
prod - Prod deploy:
docker pullna vm-apidocker compose up -d <slug>(rolling, kontejner se vymění)- Apps-gateway se reloadne (nebo discovery sám detekuje)
Versioning kontejnerů¶
Stejné jako binárky:
- alpha — interní (Avaxis QA)
- beta — vybrané firmy (per-firma routing)
- stable — všichni
Per-firma routing v gateway: header X-Company-ID: <uuid> → cluster volby (stable / beta / alpha).
9. Migrace existujících apps¶
Avax Legal (pilot)¶
- Vyhodit
_BOOTSTRAP_MODULESreferenci vdynamic_mounts.py - V
/home/avax/avax-legaldoplnitDockerfile,docker-compose.yml, health endpoint - Build image, registrovat v
apps-gatewayconfigu - Smoke test:
curl /apps/legal/health - Klient (
avax-legal-client) beze změny — volá/legal/*přes launcher → gateway → app container - Routing:
/legal/*→/apps/legal/*(gateway prefix rewrite) - NEBO klient začne volat
/apps/legal/*přímo
Postup pro nové apps¶
Po POST /admin/app-management/create:
1. Vytvořit Gitea repo (existující ✓)
2. Push code skeleton z tools/avax-app-template/ (TODO)
3. Push backend skeleton s Dockerfile + health endpoint + Alembic init (TODO)
4. Vytvořit DB schema <slug> + user avax_app_<slug> s heslem (TODO)
5. Registrovat v apps-gateway (TODO)
6. CI v Gitea automaticky postaví image (TODO)
10. Otevřené otázky¶
- Apps-gateway: vlastní FastAPI proxy nebo Traefik? Traefik má label-based discovery (kontejner se přidá → gateway ho vidí), ale je to další komponenta. FastAPI proxy je jednoduchá, ale potřebuje config push.
- JWKS endpoint — vystavit z core-api, nebo statický PEM v configu apps? JWKS umožní rotaci klíčů bez restartu, statický PEM je jednodušší.
- Cross-app komunikace — má app volat jinou app přímo (
/apps/<other>/...) nebo přes core-api? Nedoporučuji přímo (coupling). - Container registry — Gitea Container Registry funguje, ale je to nová věc. Alternativa: vlastní Harbor / Docker Hub. Začít s Gitea, posunout pokud bude omezení.
- App secrets — kde drží app vlastní credentials (např. OpenAI key pro avax-legal)? Sealed secrets (Bitnami)? Docker secrets? Hashicorp Vault? Začít s Docker secrets per service.
- Metrics endpoint —
/metrics(Prometheus) povinný? Doporučuji ano (vm-monitor scrape), ale dáme volitelný v MVP. - Rate limiting per app — kde? Gateway level (per /apps/
/* prefix). Limity per role uživatele. - Migrace Avax Legal — kdo to udělá a kdy? Vendor (qwen) sám nebo Avaxis tým?
11. Timeline¶
| Fáze | Co | Cílový termín |
|---|---|---|
| Phase 0 — Spec | Tento dokument + diskuze | 2026-05-14 ✓ |
| Phase 1a — avax-auth Python | Sdílený JWT validation package | 2026-05-14 ✓ (v0.1.0) |
| Phase 1b — RS256 + JWKS | Migrace access/refresh tokenů na RS256 + JWKS endpoint | 2026-05-14 ✓ |
| Phase 1c — Skeleton push | POST /admin/app-management/create pushuje template do avax-apps/<slug>-app |
2026-05-14 ✓ |
| Phase 1d — Foundation | apps-gateway + PgBouncer | TBD |
| Phase 2 — Pilot | Migrate avax-legal do kontejneru | TBD |
| Phase 3 — Tooling | Skeleton push do nových Gitea repos s Dockerfile + Alembic | TBD |
| Phase 4 — CI/CD | Gitea Actions build + deploy pipeline | TBD |
| Phase 5 — Production | vm-api hosting kontejnerů, monitoring, alerting | TBD |
| Phase 6 — Polyglot | avax-auth Node + Go packages, příklady non-Python app | TBD |
12. Rizika a mitigace¶
| Riziko | Pravděpodobnost | Dopad | Mitigace |
|---|---|---|---|
| PgBouncer connection storm při app restart | střední | vysoký | default_pool_size: 25, exponential backoff v app |
| JWT decode v každém app overhead | nízká | nízký | RS256 + cached public key (1h TTL) |
| Gateway single point of failure | nízká | vysoký | 2 instance gateway za Nginx load balancer |
| App push do registry selhání | střední | střední | CI retry 3x + fallback do /var/cache/images |
| Apps gateway config drift | střední | střední | Config v gitu, deploy přes ansible/CI |
| App vrátí 500 → user nevidí proč | vysoká | nízký | Strukturovaný log do stdout, agreguje Loki |
13. Otázky pro pilot (avax-legal)¶
Před migrací odpovědět:
- Jakou DB schema má
avax_legal_backend.legaldnes? Tabulky? - Volá
legaljiné moduly přímo (Python imports), nebo jen přes core API? - Závisí na Redis (Pub/Sub, cache)?
- Background úlohy (Celery)?
- Má vlastní credentials k externímu API (např. legislativní DB)?
- Velikost příchozího trafficu (požadavků za hodinu)?