Přeskočit obsah

Backend Deploy — Specifikace

Souvisí s: docs/spec/app-distribution.md (publisher-side flow — backend využívá stejnou bucket-per-app distribuci se slug backend) Tento dokument pokrývá: kompletní specifikace nasazení FastAPI backendu AVAX Platform na cílových serverech (avaxdev = beta, vm-api = stable) přes Docker compose, image v S3, sealed secrets. Status: Spec hotov 2026-05-12. Implementace = fáze F-AppDist-5.x.


1. Souhrn rozhodnutí

Rozhodnutí Volba
Runtime Docker compose (api + worker + migrate služby)
Image storage S3 tarball app-backend/versions/{ver}/image.tar.gz (žádný registry)
Service workers Stejný image, druhá compose služba worker (Celery)
Secrets Sealed secrets v S3 app-backend/secrets/{env}/.env.enc (AES-GCM)
Trigger beta (dev) systemd timer každých 5 min poll channels/beta.json
Trigger stable (prod) Webhook z portálu POST /admin/deploy/backend
Trigger hotfix systemd timer každou 1 min poll channels/hotfix.json, bypassuje normal flow
DB backup pg_dump uvnitř kontejneru před každou migrací (/var/backups/avax/)
CI Gitea Actions auto-build na push do master, auto-publish do beta
Migrace bare→docker Postupná — Docker na portu 8001 paralelně, přepnutí nginx upstream po ověření

2. Architektura

┌─────────────────────────────────────────────────────────────────────┐
│  Vendor / CI machine (avax repo, Gitea Actions)                     │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ build.sh:                                                      │  │
│  │   docker build -t avax-backend:{ver} .                         │  │
│  │   docker save avax-backend:{ver} | gzip > image.tar.gz         │  │
│  │   avax-publish app --slug backend --version {ver}              │  │
│  │     --source ./dist/{ver}/ --platform any --channel beta       │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                          ↓ S3 PUT                                    │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│  S3 (https://s3.avaxis.cz, Ceph RGW)                                │
│  app-backend/                                                       │
│    versions/{ver}/files/any/image.tar.gz  (~150 MB)                │
│    versions/{ver}/files/any/compose.yml                            │
│    versions/{ver}/files/any/migrate.sh                             │
│    versions/{ver}/version-manifest.json                            │
│    channels/{beta,stable,hotfix}.json                              │
│    secrets/{dev,prod}/.env.enc                                     │
└─────────────────────────────────────────────────────────────────────┘
                           ↓ pull
        ┌──────────────────┴──────────────────┐
        ↓                                     ↓
┌─────────────────────┐               ┌─────────────────────┐
│  avaxdev (beta)     │               │  vm-api (stable)    │
│  - systemd timer    │               │  - webhook listener │
│  - polls beta každ  │               │  - reaguje na portal│
│    5 min            │               │  - hotfix timer 1 min│
│  - hotfix timer     │               │                     │
│                     │               │                     │
│  docker compose:    │               │  docker compose:    │
│  ├─ api  (8001)     │               │  ├─ api  (8001)     │
│  ├─ worker (celery) │               │  ├─ worker (celery) │
│  └─ db (postgres)   │               │  └─ db (postgres)   │
│                     │               │                     │
│  nginx :80 → :8001  │               │  nginx :443 → :8001 │
└─────────────────────┘               └─────────────────────┘

2.1 Compose services

# /etc/avax/compose.yml (stažen z S3, atomic swap při deploy)
services:
  api:
    image: avax-backend:1.0.1
    container_name: avax-api
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000
    ports:
      - "127.0.0.1:8001:8000"   # nginx upstream
    env_file: /etc/avax/.env    # decrypted from S3 by deploy.sh
    depends_on:
      migrate:
        condition: service_completed_successfully
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 3

  worker:
    image: avax-backend:1.0.1    # stejný image jako api
    container_name: avax-worker
    command: celery -A app.workers worker --loglevel=info --concurrency=2
    env_file: /etc/avax/.env
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  migrate:
    image: avax-backend:1.0.1
    container_name: avax-migrate
    command: alembic upgrade head
    env_file: /etc/avax/.env
    depends_on:
      db:
        condition: service_healthy
    restart: "no"

  db:
    image: postgres:16-alpine
    container_name: avax-db
    environment:
      POSTGRES_DB: avaxis
      POSTGRES_USER: avaxis
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - avax-pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U avaxis"]
      interval: 10s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: avax-redis
    volumes:
      - avax-redis-data:/data
    restart: unless-stopped

volumes:
  avax-pg-data:
  avax-redis-data:

secrets:
  db_password:
    file: /etc/avax/db_password.txt   # set during bootstrap

Pozn.: Compose verzuje se 3.9+. depends_on.condition vyžaduje compose v2.

2.2 Dockerfile

# avax/backend/Dockerfile
FROM python:3.12-slim AS build
WORKDIR /build
COPY pyproject.toml requirements.txt ./
RUN pip wheel --no-deps --wheel-dir wheels -r requirements.txt
COPY . .
RUN python -m build --wheel --outdir wheels

FROM python:3.12-slim
WORKDIR /app
COPY --from=build /build/wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl

# alembic migrace zustavaji v image (importovany jako balicek)
COPY migrations /app/migrations
COPY alembic.ini /app/alembic.ini

# Healthcheck endpoint
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=3s \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)"

# Default = uvicorn (compose override pro worker / migrate)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

3. Bucket layout app-backend/

app-backend/
├── versions/
│   ├── 1.0.0/
│   │   ├── files/any/
│   │   │   ├── image.tar.gz             ← docker save | gzip (~150 MB)
│   │   │   ├── compose.yml              ← Docker compose definice
│   │   │   ├── migrate.sh               ← idempotent migration runner
│   │   │   ├── deploy.sh                ← lokální deploy skript (per verze)
│   │   │   └── ENV.md                   ← seznam povinných env proměnných
│   │   ├── version-manifest.json
│   │   └── changelog.md
│   └── 1.0.1/
├── channels/
│   ├── beta.json    {"version": "1.0.1", "promoted_at": "...", "promoted_by": "..."}
│   ├── stable.json  {"version": "1.0.0", ...}
│   └── hotfix.json  {"version": "1.0.0-hotfix.1", "expires_at": "...", "reason": "CVE-2026-xxxx"}
├── secrets/
│   ├── dev/.env.enc   ← AES-256-GCM, sealed by master.key on avaxdev
│   └── prod/.env.enc  ← AES-256-GCM, sealed by master.key on vm-api
└── deploy/
    ├── bootstrap.sh   ← prvotni install na cisty server (curl | bash)
    └── master-key.txt.example  ← format master.key (32 bytes base64)

Bucket policy (smíšená): - app-backend/deploy/*public-read (bootstrap.sh dostupný přes curl|bash, ostatní deploy skripty stahovány v rámci bootstrap přes aws cli s appdist creds — funguje obojí). - app-backend/secrets/*privátní (sealed .env.enc, ale i tak žádný anonymní GET). - app-backend/versions/*privátní (image.tar.gz vyžaduje appdist creds). - app-backend/channels/*privátní (deploy.sh čte přes appdist).


4. Sealed secrets — .env.enc

4.1 Šifra a master key

  • Algoritmus: AES-256-GCM
  • Master key: 32 bytů náhodných, base64 v /etc/avax/master.key (chmod 400, vlastník root)
  • Nonce: 12 bytů náhodných, prepended k ciphertext (nonce || tag || ciphertext)
  • Tag length: 16 bytů (GCM authentication tag)

4.2 CLI nástroj avax-secrets

# tools/secrets/avax_secrets.py — součást avax repu

# Seal — vendor / operator zaba .env do S3
avax-secrets seal \
    --env dev \
    --in ./backend/.env.dev \
    --master /etc/avax/master.key \
    --out s3://app-backend/secrets/dev/.env.enc

# Unseal — deploy.sh při deploy
avax-secrets unseal \
    --env dev \
    --in s3://app-backend/secrets/dev/.env.enc \
    --master /etc/avax/master.key \
    --out /etc/avax/.env

# Rotate — vygeneruje nový master.key, re-encrypt vsechny secrety
avax-secrets rotate \
    --old /etc/avax/master.key \
    --new /etc/avax/master.key.new \
    --envs dev,prod
# Po rotaci: zkopirovat .key.new na vsechny servery, smazat .key, prejmenovat

4.3 Bootstrap master key

První instalace serveru:

# Operator generuje master key (NE z S3, NE z CI — manualne)
head -c 32 /dev/urandom | base64 > /etc/avax/master.key
chmod 400 /etc/avax/master.key
chown root:root /etc/avax/master.key

# Stejny key musi byt na vsech serverech které čtou stejné secrets/{env}.env.enc
# = dev a prod maji RUZNE master keys (kazdý sealed pro sebe)

4.4 Lifecycle

operator .env.dev  ──seal──►  s3://app-backend/secrets/dev/.env.enc
                                        ▼ deploy.sh unseal
                                /etc/avax/.env (chmod 600)
                                        ▼ docker compose env_file
                                Backend container

Pri změně env: operator updatuje lokální .env, seal, re-deploy.


5. Build & publish

5.1 Manuální (dev / hotfix)

# avax/backend/
VERSION=1.0.1
mkdir -p /tmp/backend-${VERSION}/files/any

# 1. Build image
docker build -t avax-backend:${VERSION} .
docker save avax-backend:${VERSION} | gzip > /tmp/backend-${VERSION}/files/any/image.tar.gz

# 2. Compose + skripty (z avax repu)
cp deploy/compose.yml      /tmp/backend-${VERSION}/files/any/
cp deploy/migrate.sh       /tmp/backend-${VERSION}/files/any/
cp deploy/deploy.sh        /tmp/backend-${VERSION}/files/any/
cp deploy/ENV.md           /tmp/backend-${VERSION}/files/any/

# 3. Publish
python tools/publish/publish.py app \
    --slug backend \
    --version ${VERSION} \
    --source /tmp/backend-${VERSION}/files/any/ \
    --platform any \
    --changelog ./CHANGELOG-${VERSION}.md \
    --channel beta

5.2 CI — Gitea Actions

.gitea/workflows/backend-publish.yml:

name: Backend build & publish

on:
  push:
    branches: [master]
    paths:
      - 'backend/**'
      - 'tools/publish/**'
      - '.gitea/workflows/backend-publish.yml'

jobs:
  build-publish-beta:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Derive version
        run: |
          # version = nejnovejsi git tag + commit count + sha (např. 1.0.1+12.abc1234)
          echo "VERSION=$(git describe --tags --always | sed 's/^v//')" >> $GITHUB_ENV

      - name: Build Docker image
        run: |
          docker build -t avax-backend:${{ env.VERSION }} backend/
          mkdir -p /tmp/dist/files/any
          docker save avax-backend:${{ env.VERSION }} | gzip > /tmp/dist/files/any/image.tar.gz
          cp backend/deploy/{compose.yml,migrate.sh,deploy.sh,ENV.md} /tmp/dist/files/any/

      - name: Install avax-publish
        run: pip install -r tools/publish/requirements.txt

      - name: Publish to beta
        env:
          APPDIST_S3_ENDPOINT: ${{ secrets.APPDIST_S3_ENDPOINT }}
          APPDIST_S3_ACCESS_KEY: ${{ secrets.APPDIST_S3_ACCESS_KEY }}
          APPDIST_S3_SECRET_KEY: ${{ secrets.APPDIST_S3_SECRET_KEY }}
          AVAX_BACKEND_URL: ${{ secrets.AVAX_BACKEND_URL }}
          AVAX_API_TOKEN: ${{ secrets.AVAX_API_TOKEN }}
        run: |
          python tools/publish/publish.py app \
              --slug backend \
              --version ${{ env.VERSION }} \
              --source /tmp/dist/files/any/ \
              --platform any \
              --channel beta

CI publish je vždy do beta. Promote do stable jen ručně přes Správu portálu.

5.3 Promote stable

# Super admin v portálu klikne "Promote 1.0.1 → stable" v sekci Apps/backend
# → backend zavolá `avax-publish promote --slug backend --version 1.0.1 --to stable`
# → channels/stable.json update
# → webhook deploy na vm-api (sekce 6.2)

6. Trigger mechanismy

6.1 systemd timer (beta na avaxdev)

/etc/systemd/system/avax-backend-poll.timer:

[Unit]
Description=Poll app-backend beta channel every 5 min

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target

/etc/systemd/system/avax-backend-poll.service:

[Unit]
Description=Deploy backend if new beta version

[Service]
Type=oneshot
User=avax
EnvironmentFile=/etc/avax/poll.env   # APPDIST_S3_* pro aws cli, MASTER_KEY_PATH
ExecStart=/usr/local/bin/avax-backend-deploy beta

6.2 Webhook (stable na vm-api)

Operator klikne v portálu Správa portálu → Apps → backend → Deploy stable.

Backend (na vm-api samém, nebo na portálu) zavolá:

# Backend endpoint na portálu:
@admin_router.post("/admin/deploy/backend")
async def deploy_backend(body: DeployBackendRequest, current_user: User):
    require_super_admin(current_user)
    # body = {"target_server": "vm-api", "channel": "stable", "version": "1.0.1"}
    ssh_cmd = f"avax-backend-deploy {body.channel} --target {body.version}"
    proc = subprocess.run(
        ["ssh", "-i", "/etc/avax/deploy.ed25519", f"deploy@{body.target_server}", ssh_cmd],
        capture_output=True, timeout=600,
    )
    return DeployResult(stdout=proc.stdout.decode(), stderr=proc.stderr.decode(), exit_code=proc.returncode)

vm-api má v ~/.ssh/authorized_keys zápis:

command="/usr/local/bin/avax-backend-deploy",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAA... portal-deploy@avaxis.cz

→ omezí SSH session jen na deploy skript (žádné jiné shellové příkazy).

6.3 Hotfix timer (každý server)

Důležité (F-AppDist-5.8 validace): Hotfix verze MUSÍ být forward-compatible (stejná nebo novější alembic migration head). Hotfix backward na starší verzi s méně migrations selže v migrate kontejneru (alembic odmítne downgrade). Při selhání se rollback NEAPLIKUJE automaticky (stack zůstává na současné verzi), channels/hotfix.json se nesmaže — operator musí manuálně přes DELETE /admin/apps/backend/hotfix.

/etc/systemd/system/avax-backend-hotfix.timer:

[Unit]
Description=Hotfix poll — emergency deploy bypass channel

[Timer]
OnBootSec=30s
OnUnitActiveSec=1min     # každou minutu, kvuli rychlost na CVE
Persistent=true

[Install]
WantedBy=timers.target

/etc/systemd/system/avax-backend-hotfix.service:

[Service]
Type=oneshot
User=avax
EnvironmentFile=/etc/avax/poll.env
ExecStart=/usr/local/bin/avax-backend-deploy hotfix

avax-backend-deploy hotfix chování:

1. GET s3://app-backend/channels/hotfix.json
2. Pokud 404 → exit 0 (no hotfix)
3. Pokud existuje a {version} != aktualni:
   - Override channels/beta nebo stable
   - Spusti standardni deploy flow (download image, migrate, restart)
   - PRESKAKUJE: pg_dump (rychlost na CVE)
                   manual approval (autonomne)
4. Po uspesnem deploy: DELETE channels/hotfix.json (backend API, jen super_admin)

Hotfix promote:

# Pouze super_admin přes portál: Apps → backend → Emergency Hotfix
# Body: {"version": "1.0.0-hotfix.1", "reason": "CVE-2026-xxxx", "ttl_hours": 24}
# Backend: zapis channels/hotfix.json + audit log + email notif vsem super_adminum

7. avax-backend-deploy skript

/usr/local/bin/avax-backend-deploy (instalovany pri bootstrap):

#!/usr/bin/env bash
set -euo pipefail

CHANNEL="${1:?Usage: avax-backend-deploy {beta|stable|hotfix} [--target VERSION]}"
TARGET_VERSION=""
if [[ "$@" =~ --target ]]; then
    TARGET_VERSION="$3"
fi

# Načti env (APPDIST_S3_*, MASTER_KEY_PATH, ENV_NAME)
source /etc/avax/poll.env

# Lock — zabran konkurentnim deployum
LOCK=/var/lock/avax-backend-deploy.lock
exec 200>"$LOCK"
flock -n 200 || { echo "Deploy uz bezi (lock)"; exit 1; }

# 1. Zjisti cilovou verzi
if [[ -z "$TARGET_VERSION" ]]; then
    TARGET_VERSION=$(aws s3 cp --endpoint-url "$APPDIST_S3_ENDPOINT" \
        "s3://app-backend/channels/${CHANNEL}.json" - | jq -r .version)
fi

CURRENT=$(cat /etc/avax/current_version 2>/dev/null || echo "none")
if [[ "$TARGET_VERSION" == "$CURRENT" ]]; then
    echo "Aktualizovan na ${TARGET_VERSION} (${CHANNEL}). NIC."; exit 0
fi

echo "[*] Deploy ${CHANNEL}: ${CURRENT} -> ${TARGET_VERSION}"

# 2. Stahni soubory
TMPDIR=$(mktemp -d)
aws s3 cp --endpoint-url "$APPDIST_S3_ENDPOINT" --recursive \
    "s3://app-backend/versions/${TARGET_VERSION}/files/any/" "${TMPDIR}/"

# 3. Decrypt secrets
aws s3 cp --endpoint-url "$APPDIST_S3_ENDPOINT" \
    "s3://app-backend/secrets/${ENV_NAME}/.env.enc" "${TMPDIR}/.env.enc"
python3 /usr/local/lib/avax/secrets.py unseal \
    --in "${TMPDIR}/.env.enc" \
    --master "$MASTER_KEY_PATH" \
    --out "${TMPDIR}/.env"

# 4. Load Docker image
gunzip -c "${TMPDIR}/image.tar.gz" | docker load
# Image tag = avax-backend:{TARGET_VERSION}

# 5. DB backup (skip pri hotfix)
if [[ "$CHANNEL" != "hotfix" ]]; then
    BACKUP_DIR=/var/backups/avax
    mkdir -p "$BACKUP_DIR"
    BACKUP_FILE="${BACKUP_DIR}/pre-${TARGET_VERSION}-$(date +%Y%m%d-%H%M%S).dump"
    echo "[*] pg_dump -> ${BACKUP_FILE}"
    docker exec avax-db pg_dump -U avaxis -F c -d avaxis > "$BACKUP_FILE"
    # Retention: smaze zalohy starsi 30 dni
    find "$BACKUP_DIR" -name "pre-*.dump" -mtime +30 -delete
fi

# 6. Backup compose + .env (pro rollback)
cp /etc/avax/compose.yml /etc/avax/compose.yml.prev 2>/dev/null || true
cp /etc/avax/.env /etc/avax/.env.prev 2>/dev/null || true
cp "${TMPDIR}/compose.yml" /etc/avax/compose.yml
mv "${TMPDIR}/.env" /etc/avax/.env
chmod 600 /etc/avax/.env

# Update image tag v compose.yml na TARGET_VERSION
sed -i "s|avax-backend:.*|avax-backend:${TARGET_VERSION}|g" /etc/avax/compose.yml

# 7. docker compose up — migrate prvni, pak api+worker
echo "[*] docker compose up migrate (alembic upgrade)"
cd /etc/avax && docker compose up -d migrate
docker compose wait migrate
MIGRATE_EXIT=$(docker inspect avax-migrate --format '{{.State.ExitCode}}')
if [[ "$MIGRATE_EXIT" != "0" ]]; then
    echo "FAIL: migrate exit ${MIGRATE_EXIT} — viz docker logs avax-migrate"
    docker logs --tail 50 avax-migrate
    # Rollback
    avax-backend-deploy --rollback
    exit 2
fi

# 8. Restart api + worker
echo "[*] docker compose up -d (api, worker)"
docker compose up -d api worker

# 9. Healthcheck
HEALTH_OK=false
for i in 1 2 3 4 5; do
    sleep 3
    if curl -fsS http://localhost:8001/health > /dev/null; then
        HEALTH_OK=true; break
    fi
    echo "[*] healthcheck attempt $i/5..."
done

if ! $HEALTH_OK; then
    echo "FAIL: healthcheck po 15s"
    avax-backend-deploy --rollback
    exit 3
fi

# 10. Update markers
echo "$TARGET_VERSION" > /etc/avax/current_version
echo "$CURRENT" > /etc/avax/previous_version

# 11. Po uspesnem hotfix deploy: smaz channels/hotfix.json
if [[ "$CHANNEL" == "hotfix" ]]; then
    curl -fsS -X DELETE -H "Authorization: Bearer $AVAX_API_TOKEN" \
        "$AVAX_BACKEND_URL/admin/apps/backend/hotfix"
fi

rm -rf "$TMPDIR"
echo "[OK] Deploy ${TARGET_VERSION} (${CHANNEL}) — healthcheck 200, vsechno bezi"

7.1 Rollback subcommand

# avax-backend-deploy --rollback
# Vrati compose.yml.prev + .env.prev, docker compose recreate
PREV=$(cat /etc/avax/previous_version)
echo "[*] Rollback na ${PREV}"
cp /etc/avax/compose.yml.prev /etc/avax/compose.yml
cp /etc/avax/.env.prev /etc/avax/.env
cd /etc/avax && docker compose up -d api worker
echo "$PREV" > /etc/avax/current_version

# Pozn.: DB migrace se NEROLLBACKUJE.
# Pokud nova verze udelala forward migrace nekompatibilni s prev:
#   1. Manualni: pg_restore z /var/backups/avax/pre-{ver}-*.dump
#   2. Nebo: psql -U avaxis -d avaxis -f <(alembic downgrade -- --sql)
# Tymto skript varuje:
if [[ -f /var/backups/avax/pre-${PREV_VERSION}-*.dump ]]; then
    echo "[!] DB pripadne v nove schema. Pro DB rollback: pg_restore z /var/backups/avax/pre-*.dump"
fi

8. DB migrace a backup

8.1 Migrace timing

deploy.sh:
  1. pg_dump (60-300s pro typickou avax DB)
  2. docker compose up migrate (alembic upgrade head)
  3. docker compose wait migrate  ← blokuje az do completion
  4. Pokud migrate.exit != 0:
     - Rollback compose + .env
     - Migrace v PG zustane (alembic nikdy automaticky nedown-gradeuje)
     - Operator manualne: pg_restore z dump nebo alembic downgrade
  5. Pokud OK: pokracuj na api+worker restart

8.2 Backup retention

/var/backups/avax/
├── pre-1.0.0-20260512-103015.dump   ← per-deploy snapshot
├── pre-1.0.1-20260512-150022.dump
└── daily/
    ├── avax-20260510.dump.gz          ← cron @ 02:00 daily
    └── avax-20260511.dump.gz
# /etc/cron.d/avax-backup
0 2 * * * avax docker exec avax-db pg_dump -U avaxis -F c -d avaxis | gzip > /var/backups/avax/daily/avax-$(date +\%Y\%m\%d).dump.gz
# Retention: 90 dni
0 3 * * * avax find /var/backups/avax/daily -name "*.dump.gz" -mtime +90 -delete
0 3 * * * avax find /var/backups/avax -name "pre-*.dump" -mtime +30 -delete

8.3 Restore postup

# 1. Stop api + worker (drz db)
docker compose stop api worker

# 2. Drop a recreate DB (NEBEZPECNE — nutna potvrzeni)
docker exec avax-db psql -U postgres -c "DROP DATABASE avaxis; CREATE DATABASE avaxis OWNER avaxis;"

# 3. Restore
docker exec -i avax-db pg_restore -U avaxis -d avaxis < /var/backups/avax/pre-1.0.0-20260512-103015.dump

# 4. Restart api + worker
docker compose up -d api worker

# 5. Verify
curl -f http://localhost:8001/health && echo "OK"

9. First-time bootstrap

s3://app-backend/deploy/bootstrap.sh:

#!/usr/bin/env bash
# Curl from new server:
#   curl -fsSL https://s3.avaxis.cz/app-backend/deploy/bootstrap.sh | sudo bash -s -- {dev|prod}

set -euo pipefail

ENV_NAME="${1:?Usage: bootstrap.sh {dev|prod}}"
INSTALL_DIR=/etc/avax
APPDIR=/usr/local/lib/avax

echo "==> AVAX Backend Bootstrap (${ENV_NAME})"

# 1. Predpoklady
which docker || { echo "FAIL: nainstaluj Docker"; exit 1; }
which docker-compose || which docker compose || { echo "FAIL: docker compose v2"; exit 1; }
which jq aws curl gpg || apt install -y jq awscli curl gnupg

# 2. Vytvor avax usera + adresare
useradd -r -s /bin/bash -d /home/avax -m avax 2>/dev/null || true
mkdir -p "$INSTALL_DIR" "$APPDIR" /var/backups/avax /var/log/avax
chown avax:avax /var/backups/avax /var/log/avax

# 3. Master key (operator manualne — NIKDY z internetu)
if [[ ! -f "$INSTALL_DIR/master.key" ]]; then
    echo
    echo "VYZADUJE RUCNI KROK:"
    echo "  1. Vygeneruj 32-byte key (head -c 32 /dev/urandom | base64)"
    echo "  2. Zapis do ${INSTALL_DIR}/master.key (chmod 400, owner root)"
    echo "  3. Stejny key musi byt pristupny i pro super_admin (Bitwarden vault) pro rotaci"
    echo "  4. Re-run bootstrap.sh"
    exit 2
fi
chmod 400 "$INSTALL_DIR/master.key"

# 4. APPDIST credentials (vyzadi se interaktivne nebo z env)
if [[ ! -f "$INSTALL_DIR/poll.env" ]]; then
    read -p "APPDIST_S3_ACCESS_KEY: " AK
    read -s -p "APPDIST_S3_SECRET_KEY: " SK
    cat > "$INSTALL_DIR/poll.env" <<EOF
APPDIST_S3_ENDPOINT=https://s3.avaxis.cz
APPDIST_S3_ACCESS_KEY=${AK}
APPDIST_S3_SECRET_KEY=${SK}
APPDIST_S3_REGION=us-east-1
AVAX_BACKEND_URL=https://api.avaxis.cz
ENV_NAME=${ENV_NAME}
MASTER_KEY_PATH=${INSTALL_DIR}/master.key
EOF
    chmod 600 "$INSTALL_DIR/poll.env"
fi

# 5. Stahni avax-backend-deploy skript + secrets unsealer
aws s3 cp --endpoint-url https://s3.avaxis.cz \
    s3://app-backend/deploy/avax-backend-deploy /usr/local/bin/avax-backend-deploy
chmod +x /usr/local/bin/avax-backend-deploy
aws s3 cp --endpoint-url https://s3.avaxis.cz \
    s3://app-backend/deploy/secrets.py "$APPDIR/secrets.py"

# 6. Systemd units (timer pro hotfix vždy, beta-timer jen pro dev)
aws s3 cp --recursive --endpoint-url https://s3.avaxis.cz \
    s3://app-backend/deploy/systemd/ /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now avax-backend-hotfix.timer
if [[ "$ENV_NAME" == "dev" ]]; then
    systemctl enable --now avax-backend-poll.timer
fi

# 7. Init deploy — pull stable
echo "==> Pocatecni deploy (channel=stable)"
/usr/local/bin/avax-backend-deploy stable

# 8. Nginx config
aws s3 cp --endpoint-url https://s3.avaxis.cz \
    s3://app-backend/deploy/nginx-avax-api.conf /etc/nginx/sites-available/avax-api
ln -sf /etc/nginx/sites-available/avax-api /etc/nginx/sites-enabled/avax-api
nginx -t && systemctl reload nginx

echo "==> Bootstrap hotov. Backend bezi na portu 8001 (proxy 443->8001 v nginx)."

10. Postupná migrace bare uvicorn → Docker

Faze A (priprava):
  - avaxdev: docker compose stack pripraven na portu 8001
  - bare uvicorn (port 8000) bezi normalne
  - aplikace: pres nginx volaji port 8000

Faze B (test):
  - smoke test docker compose stack: curl http://localhost:8001/health
  - integration test: registrace, login, chat send/recv pres docker stack
  - prubezne porovnavat docker i bare odpovedi (curl test)

Faze C (cutover):
  - V nginx config: upstream { server localhost:8000; backup server localhost:8001; }
  - reload nginx — zadny dopad
  - Pak: zameni se primary/backup:
        upstream { server localhost:8001; backup server localhost:8000; }
  - reload nginx — backend traffic ide na docker
  - 24h observation (logy, metriky)

Faze D (cleanup):
  - pkill -f "uvicorn backend.app.main" — vypne bare uvicorn
  - Smaze /home/avax/avax-platform/backend/.venv (uz neni potreba)
  - Update systemd: disable bare uvicorn unit (pokud bylo)

Stejny postup pro prod (vm-api), ale po overeni na dev.


11. Portal UI — Správa portálu → Backend Deploy

Endpointy:

Metoda Path Role Popis
GET /admin/deploy/backend/status super_admin Vraci {server, current_version, channel, last_deploy, healthcheck_ok} per server
GET /admin/apps/backend/versions catalog_admin Seznam vsech publikovanych verzi backendu
POST /admin/deploy/backend super_admin Spusti webhook deploy pro body.target_server na body.channel
POST /admin/apps/backend/hotfix super_admin Nastav hotfix.json (body: version, reason, ttl_hours)
DELETE /admin/apps/backend/hotfix super_admin Smaz hotfix.json (typicky po uspesnem deploy automatic)
GET /admin/audit/deploys super_admin Log vsech deploys (kdo, kdy, vysledek)

UI (Správa portálu → Apps → backend):

┌─────────────────────────────────────────────────────────────────┐
│ Backend (avax-platform FastAPI)                                 │
│                                                                 │
│ Server stav:                                                    │
│   avaxdev (beta)  │ 1.0.1  │ healthy │ 2026-05-12 13:45 │ auto │
│   vm-api (stable) │ 1.0.0  │ healthy │ 2026-05-11 09:00 │ ----- │
│                                                                 │
│ Dostupne verze:                                                 │
│   1.0.2  (beta, 2026-05-12 14:00)  [Promote stable]            │
│   1.0.1  (beta)                                                 │
│   1.0.0  (stable)                                               │
│                                                                 │
│ [+ Promote 1.0.2 -> stable]   [Hotfix...]                       │
└─────────────────────────────────────────────────────────────────┘

12. Implementační plán

F-AppDist-5.1  Dockerfile + image build flow            (~2-3 dny)
              - backend/Dockerfile (multi-stage)
              - backend/deploy/{compose.yml, migrate.sh, deploy.sh, ENV.md}
              - build skript v Makefile / build.sh

F-AppDist-5.2  Sealed secrets implementace              (~1 den)
              - tools/secrets/avax_secrets.py (click CLI: seal/unseal/rotate)
              - tests pro AES-GCM round-trip
              - dokumentace v secrets/README.md

F-AppDist-5.3  avax-backend-deploy skript + bootstrap   (~2 dny)
              - skript pro deploy/rollback
              - bootstrap.sh pro prvotni install
              - systemd units (timer + service pro beta + hotfix)

F-AppDist-5.4  CI Gitea Actions auto-build              (~1 den)
              - .gitea/workflows/backend-publish.yml
              - secrets v Gitea (APPDIST_S3_*, AVAX_API_TOKEN)
              - first build na master push

F-AppDist-5.5  Backend portal endpoints                 (~2 dny)
              - /admin/deploy/backend/status (GET)
              - /admin/deploy/backend (POST webhook trigger)
              - /admin/apps/backend/hotfix (POST/DELETE)
              - /admin/audit/deploys (GET)
              - portal UI (Next.js -- zatim mockup)

F-AppDist-5.6  Migrace dev z bare uvicorn               (~1 den, koordinovano)
              - bootstrap avaxdev na docker, port 8001
              - nginx switch upstream
              - 24h monitor
              - cleanup bare uvicorn

F-AppDist-5.7  Migrace prod (vm-api)                    (~1 den, planovany window)
              - shoda postup dle 5.6, po overeni na dev

F-AppDist-5.8  Smoke test full E2E                      (~1 den)
              - tag v1.0.1 -> CI -> beta -> avaxdev deploy automaticky
              - manual promote stable -> vm-api webhook deploy
              - test hotfix flow (set hotfix.json -> oba servery do 1 min)
              - test rollback (umely fail healthcheck)

13. Otevřené otázky (rozhodnout během implementace)

  1. Image squashingdocker save | gzip produces ~150 MB. docker buildx --squash může snížit na ~80 MB ale komplikuje cache. Default = no squash; revisit pokud bandwidth issue.
  2. Image deduplikace — Postupne pribuva versions/{ver}/files/any/image.tar.gz s overlapping layers. Future: layer-based push (registry by spotreboval mene), ale rozhodli jsme se pro S3 flat. Lifecycle policy archiv po 90 dnech sekce 11 (app-distribution.md).
  3. Concurrent deploysflock v deploy.sh zabráni paralelnim. Pokud uživatel zmackne "Deploy" dvakrat rychle v portalu, druhy call HTTP 409.
  4. Multi-server stable — prod ma vm-api jako single instance. Pokud bude scaled (2+ instance), webhook musi nasypat na vsechny postupne (s drain via nginx).
  5. Worker scaling — celery concurrency=2 hardcoded v compose. Pro vetsi load: separe docker-compose.override.yml per env s overridem command.
  6. PG on dedicated VM (vm-db) vs docker compose — dev pouziva docker compose vsechno. Prod muze chtit PG na vm-db (per server-infrastructure.md) -> deploy.sh musi byt schopen docker compose bez db service + ENV.DATABASE_URL na vm-db.
  7. TLS termination — nginx (vm-gateway) terminuje TLS, backend uvicorn HTTP only. Po migraci ne-prepisovat.

14. Souvislosti


Aktualizováno: 2026-05-12