Backend Deploy — Specifikace¶
Souvisí s:
docs/spec/app-distribution.md(publisher-side flow — backend využívá stejnou bucket-per-app distribuci se slugbackend) 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.conditionvyž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řescurl|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 dostablejen 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.jsonse nesmaže — operator musí manuálně přesDELETE /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)¶
- Image squashing —
docker save | gzipproduces ~150 MB.docker buildx --squashmůže snížit na ~80 MB ale komplikuje cache. Default = no squash; revisit pokud bandwidth issue. - 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).
- Concurrent deploys —
flockv deploy.sh zabráni paralelnim. Pokud uživatel zmackne "Deploy" dvakrat rychle v portalu, druhy call HTTP 409. - Multi-server stable — prod ma
vm-apijako single instance. Pokud bude scaled (2+ instance), webhook musi nasypat na vsechny postupne (s drain via nginx). - Worker scaling — celery concurrency=2 hardcoded v compose. Pro vetsi load: separe
docker-compose.override.ymlper env s overridem command. - 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 composebezdbservice + ENV.DATABASE_URL na vm-db. - TLS termination — nginx (vm-gateway) terminuje TLS, backend uvicorn HTTP only. Po migraci ne-prepisovat.
14. Souvislosti¶
- Spec publisher-side:
app-distribution.md(bucket-per-app, avax-publish CLI, finalize) - Spec katalog/consumer-side:
app-catalog.md(manifest update flow v launcheru) - Infrastruktura:
server-infrastructure.md(VM layout) - Existujici deploy spec (predchozi iterace):
deployment.md(installer/bootstrapper pro launcher2)
Aktualizováno: 2026-05-12