--- source_sha: "0d080c65b077" --- # Auf einem Server deployen { #deploy-to-a-server } Ein Host, Docker Compose und ein Reverse Proxy bleiben der ausgelieferte Weg für einen einzelnen Server. Für Cluster gibt es das gepflegte [Kubernetes-Helm-Chart](kubernetes.md) mit externen Datendiensten und einem blockierenden Migrationsjob. Die OpenShift-Konfiguration ist noch nicht validiert. Ein-Klick-Schnellstarts für PaaS-Anbieter gibt es nicht. Über einen Host hinaus - ein verwaltetes Postgres, mehrere API-Hosts, mehrere Deployments - führt [Über einen Host hinaus skalieren](scaling.md). !!! tip "Lesen Sie zuerst die [Produktions-Checkliste](configuration.md#production-checklist)" Neun Einstellungen werden mit Standardwerten ausgeliefert, die auf einem Laptop in Ordnung und auf einem Host, den jemand anderes erreichen kann, falsch sind. `scripts/server-init.sh` weiter unten erzeugt alle neun, die Checkliste ist also das, was Sie danach prüfen, und nicht das, was Sie tippen. ## Was Sie brauchen { #what-you-need } | | | |---|---| | **Einen Host** | 4 vCPU und 8 GB RAM genügen. Siehe [Dimensionierung](#sizing-the-host) | | **Container-Engine** | Engine 24+ mit dem Compose-Plugin (2.24 oder neuer), und Ihr Benutzer in der Gruppe `docker`. Auf dem Host wird nichts gebaut: die Images werden von GHCR geholt. Alternativ nutzen Sie den [manuellen Podman-Weg](#podman-support). | | **Zwei Hostnamen** | einen für die Website, einen für die API — siehe [warum zwei](#why-two-hostnames) | | **Einen Reverse Proxy** | [Traefik](#option-a-traefik) oder [Nginx](#option-b-nginx). Er terminiert TLS | | **Keinen Provider-Key** | Chat-Modelle und Embeds werden beide pro Organisation im Vault des Produkts hinterlegt. Die Umgebung hält für keines von beiden Zugangsdaten — siehe [Datenschutz](data-protection.md#nothing-leaves-by-default) | Der Host braucht außerdem die Ports 80 und 443 offen, und sonst nichts. Postgres und Redis sind auf keiner Schnittstelle veröffentlicht, und der Worker bedient nichts. ### Warum zwei Hostnamen { #why-two-hostnames } Der Browser spricht mit beiden. Die meisten Aufrufe laufen über die serverseitigen Routen des Frontends, aber der Chat-WebSocket verbindet sich direkt mit der API, also braucht die API einen Namen, den ein Browser auflösen kann, und ein eigenes Zertifikat. `app.example.com` und `api.example.com` ist die Form. Es können zwei beliebige Namen sein; was sie nicht sein dürfen, ist ein Name mit einem Pfadpräfix, denn die Cookies der API und die der Website sind auf den Host begrenzt. ### Podman-Unterstützung { #podman-support } **Podman wird über die manuelle Installation mit Docker Compose als Provider unterstützt.** Getestet wurden Podman 6.1.4 und Docker Compose v2.40.3 auf Fedora CoreOS 44 (Podman-VM unter macOS, ARM64, cgroups v2, SELinux enforcing). Die gelieferte Datei benötigt Compose 2.24 oder neuer. **Verwenden Sie nicht `podman-compose` 1.6.0.** Der Stack startete und ein Agent antwortete, aber eine gezielt fehlgeschlagene Migration verhinderte den Start der abhängigen App nicht. Docker Compose verweigerte diesen Start korrekt. Die Versionsnummern der Provider sind unabhängig; `podman-compose` 1.6.0 ist nicht Docker Compose 1.6.0. Der Quickstart bleibt auf Docker beschränkt und verweist erkanntes Podman auf diesen manuellen Weg, auch mit `--no-sandbox`. Wählen Sie im konfigurierten Repository-Hauptverzeichnis die echte Docker-Compose-Datei (eigenständiges Programm oder CLI-Plugin): ```bash export PODMAN_COMPOSE_PROVIDER=/absolute/path/to/docker-compose podman compose version podman compose -f docker-compose.yml up -d ``` Prüfen Sie, dass `podman compose version` Docker Compose identifiziert. Das explizite `-f` schließt den lokalen Quellcode-Build-Override aus; diese Installation verwendet veröffentlichte Images. Folgen Sie nach dem Start [Installation](install.md), um Organisation und Modell einzurichten. **Optionale Sandbox.** `pydantic-ai-backend` 0.2.39 funktionierte mit rootful und rootless Podman-Sockets. Ein neues Backend oder Release ist nicht nötig. Verwenden Sie `docker-compose-podman.yml` für die Socket- und Workspace-Mounts; bereiten Sie deren Berechtigungen vor dem Aktivieren des Profils vor. Auf nativem Linux mit rootless Podman: ```bash systemctl --user enable --now podman.socket export SANDBOX_DOCKER_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock" export SANDBOX_WORKSPACE_ROOT="$HOME/.local/share/agenticos/sandbox-workspaces" mkdir -p "$SANDBOX_WORKSPACE_ROOT" podman unshare chown 10001:10001 "$SANDBOX_WORKSPACE_ROOT" export DOCKER_GID=0 ``` Aktivieren Sie für rootful Linux stattdessen den systemweiten `podman.socket`, setzen Sie `SANDBOX_DOCKER_SOCKET=/run/podman/podman.sock` und erstellen Sie den Workspace mit `sudo install -d -o 10001 -g 10001 "$SANDBOX_WORKSPACE_ROOT"`. Richten Sie Compose auf diese rootful Engine. Die Socket-Gruppe ist gewöhnlich 0; bei einem angepassten rootful Socket gehört seine tatsächliche Gruppe in `DOCKER_GID`. Bei einer rootless Podman-VM unter macOS liegen beide Bind-Quellen in der Linux-VM, nicht auf macOS. Bereiten Sie sie innerhalb der VM vor: ```bash export SANDBOX_DOCKER_SOCKET="$(podman machine ssh 'printf "%s/podman/podman.sock" "$XDG_RUNTIME_DIR"')" export SANDBOX_WORKSPACE_ROOT=/tmp/agenticos-sandbox-workspaces podman machine ssh 'mkdir -p /tmp/agenticos-sandbox-workspaces && podman unshare chown 10001:10001 /tmp/agenticos-sandbox-workspaces' export DOCKER_GID=0 ``` Behalten Sie bestehende Deployment-Schlüssel. Setzen Sie ein nichtleeres `SANDBOXD_TOKEN` und `SANDBOX_URL=http://sandboxd:8080` in der vom Stack gelesenen Env-Datei, wie unter [Sandbox-Konfiguration](sandbox.md) beschrieben. Starten Sie dann mit beiden Dateien: ```bash podman compose -f docker-compose.yml -f docker-compose-podman.yml \ --profile sandbox up -d ``` Der Override deaktiviert SELinux-Labeling nur für `sandboxd`, das bereits den Engine-Socket hält. Der Workspace erhält mit `:z` ein gemeinsames Label, da ihn auch benachbarte Sandboxes mounten. Sandbox-Container behalten eigene Labels und `no-new-privileges`; ändern Sie nicht das Socket-Label und deaktivieren Sie SELinux nicht global. Rootless Socket-Eigentum wird im User-Namespace auf Gruppe 0 abgebildet. Verwenden Sie auf Servern einen dauerhaften Workspace-Pfad: `/tmp` ist nur ein lokales Beispiel. Behalten Sie Socket-, Workspace- und Gruppenwerte für spätere Compose-Aufrufe in der Operator-Umgebung. `DOCKER_HOST` auf dem Host ändert nicht den Socket-Mount in `sandboxd`. Verifiziert wurden ein HTTP-Agent-Run mit gespeicherter Fertigstellung, Workspace-Dateien, Fehlercodes, Timeouts, wirksame RAM/CPU/PID-Limits und Runtime-Builds. Das gelieferte Workbench-Image las/schrieb XLSX, zeichnete einen Plot und startete Node und LibreOffice. Das Modell war ein deterministischer OpenAI-kompatibler Testserver; Desktop-Runtime und sämtliche Linux-Distributionen wurden nicht getestet. Der [Testbericht](https://github.com/vstorm-co/agenticos/blob/main/docs/audits/podman-2026-11-03.md) nennt Versionen, Umfang und die Reproduktion des Migrationsfehlers. ## Den Host dimensionieren { #sizing-the-host } Auf einem im Leerlauf befindlichen Deployment gemessen, nicht geschätzt: | | im Ruhezustand | Obergrenze | |---|---|---| | `app` (2 uvicorn workers) | ~1,0 GB | 2,5 GB bei den standardmäßigen 4 Workern | | `db` | ~1,3 GB mit dem Tuning unten | 2 GB | | `worker` (ein Ingest-Kindprozess) | 387 MiB | 2,5 GB (`WORKER_MEMORY_LIMIT`) | | `frontend` | ~300 MB | 1 GB | | `redis` | 9 MiB | 512 MB | Die Zahl, die über den Host entscheidet, ist **`UVICORN_WORKERS`**. Jeder Worker ist ein eigener Prozess, der die gesamte Anwendung importiert — 460 MiB, per Spawn statt per Fork erzeugt, es wird also nichts geteilt. Vier davon sind 1,9 GB, bevor eine Anfrage eintrifft. Zwei Worker passen zu einem Team von zehn Personen und lassen immer noch einen bedienen, während [der Watchdog](configuration.md#a-worker-whose-event-loop-has-stopped-turning) ein festgefahrenes Geschwister ersetzt. Ein Worker ist die Einstellung, die es zu vermeiden gilt: eine blockierte Event Loop ist dann das gesamte Deployment, bis es sich selbst beendet. Ein Host, dem CPU fehlt, zeigt ein anderes Symptom: Zeilen `Child process [N] died` im Log von `app` ohne Watchdog-Urteil darüber, während jede Seite langsam ist. Das sind Worker, die uvicorns Supervisor getötet hat, weil sie seinen Ping zu spät beantwortet haben, keine Worker, die ausgefallen sind. Er wartet [`UVICORN_WORKER_HEALTHCHECK_TIMEOUT`](configuration.md#a-worker-whose-event-loop-has-stopped-turning) Sekunden, standardmäßig 60. Prüfe den CPU-Steal des Hosts, bevor du den Wert erhöhst. Zwei Einstellungen halten ein langsames Deployment diagnostizierbar: - **Jedes Log ist begrenzt.** Jeder Dienst in den Produktions-Compose-Dateien schreibt höchstens fünf Dateien zu 20 MB. Docker behält das Log eines Containers standardmäßig, bis der Container entfernt wird, und nichts auf dem Host rotiert es. - **Postgres zeichnet auf, was jede Anweisung kostet.** Der Dienst `db` lädt `pg_stat_statements` beim Start vor, und eine Migration legt die Erweiterung an. Wo die Datenbankrolle sie nicht anlegen darf, protokolliert die Migration einen Hinweis und läuft weiter. `agenticos cmd doctor` gibt in jedem Fall eine Zeile `query statistics` aus. !!! note "Die Datenbank ist auf ihr eigenes Limit hin abgestimmt" `docker-compose-prod.yml` betreibt Postgres mit `shared_buffers=512MB` gegen ein Limit von 2 GB und gibt ihm 512 MB `/dev/shm` — Dockers Standard sind 64 MB, die ein paralleler Scan über die Vektoren einer Collection erschöpft, mit der Meldung `could not resize shared memory segment`. Verschieben Sie das Limit, verschieben Sie das Tuning mit; sie stehen aus diesem Grund nebeneinander. Außerdem setzt sie `max_connections=200`, denn die Pools können mehr öffnen als die 100 von Postgres ab Werk: zwei Pools zu 15 je uvicorn-Worker, also 120 bei den standardmäßigen vier, und 30 weitere für den Hintergrund-Worker. Erhöhen Sie den Wert mit `UVICORN_WORKERS` oder `DB_POOL_SIZE`, sonst lehnt Postgres Verbindungen mit „too many clients“ ab, bevor ein Pool sie einreihen oder die Last abwerfen kann. ## Die Namen auf den Host zeigen lassen { #point-the-names-at-the-host } Zwei A-Records, vor allem anderen. Let's Encrypt weist nach, dass Sie einen Namen kontrollieren, indem es eine Datei über HTTP von dort abruft, wohin der Name auflöst; ein Zertifikat wird also erst ausgestellt, wenn das zutrifft und propagiert ist. ``` app.example.com A 203.0.113.10 api.example.com A 203.0.113.10 ``` !!! warning "Ein Wildcard erledigt das nicht für Sie" Wo `*.example.com` bereits irgendwohin zeigt — meist auf eine Marketing-Website — lösen beide Namen dorthin auf. Ein Record für den konkreten Namen schlägt einen Wildcard, die Lösung besteht also darin, die beiden oben hinzuzufügen, und nicht darin, den Wildcard zu entfernen. Prüfen Sie von einem Ort, der nicht der Host ist, denn der Host kann eine eigene Antwort haben: ```bash dig +short app.example.com api.example.com ``` ## Es auf den Host bringen { #get-it-onto-the-host } ```bash sudo install -d -o "$USER" -g "$USER" /opt/agenticos git clone https://github.com/vstorm-co/agenticos.git /opt/agenticos cd /opt/agenticos bash scripts/server-init.sh ``` `server-init.sh` schreibt `backend/.env`: es erzeugt die fünf Secrets, fragt nach den beiden Hostnamen und nach einer Adresse für Let's Encrypt und leitet die öffentlichen URLs und den CORS-Origin aus Ihren Angaben ab. Nach einem Provider-Key fragt es nicht: die liegen im Vault der jeweiligen Organisation. Eine bestehende Datei überschreibt es nicht. Der Klon ist der Ort, an dem die Compose-Dateien und diese env-Datei liegen; von dort läuft kein Code. Was läuft, sind die beiden Images, die das Repository veröffentlicht. ### Die Images { #the-images } | | | |---|---| | `ghcr.io/vstorm-co/agenticos-backend` | Die API, der Worker und die Migrationen - ein Image, drei Kommandos | | `ghcr.io/vstorm-co/agenticos-frontend` | Die Konsole | Beide werden von `.github/workflows/images.yml` für `linux/amd64` und `linux/arm64` gebaut. Ein Release (`v0.0.380`) veröffentlicht `0.0.380` und verschiebt `latest`; jeder Commit auf `main` veröffentlicht `edge` und `sha-`. Die Compose-Dateien lesen den Tag aus `AGENTICOS_VERSION` in `backend/.env` und fallen auf `latest` zurück. Drei Regeln, die der Workflow einhält, jede davon wissenswert, bevor Sie sich auf einen Tag verlassen: - **Nur ein Commit auf `main` wird jemals veröffentlicht.** Ein `v*`-Tag, der von einem Branch aus gesetzt wird, oder ein dort gestarteter Lauf, wird abgelehnt, bevor irgendetwas gebaut wird - `latest` kann also nicht über die Grenze des Pull Requests hinausgelangen. - **Ein Release auf einem Commit, den `main` bereits gebaut hat, wird nicht neu gebaut.** Sein `sha-`-Manifest erhält die Version und `latest` als zusätzliche Namen, damit die Digests, auf die ein Host gepinnt ist, genau die sind, die das Release benennt. - **Ein Commit ohne Images kann sie bekommen.** Starten Sie den Workflow von Hand mit seiner `sha`-Eingabe - `gh workflow run images.yml --ref main -f sha=` - und er veröffentlicht den `sha-`-Tag dieses Commits und nichts, was sich bewegt. Das ist der Weg für einen Commit, der älter ist als der Workflow, und für einen, dessen Lauf verloren ging. !!! warning "Pinnen Sie ein Release auf einem Host, der Ihnen wichtig ist" `AGENTICOS_VERSION=0.0.380` in `backend/.env`, damit `make prod` an einem schlechten Tag das holt, was gestern lief, und nicht das, was heute Morgen veröffentlicht wurde. `scripts/deploy.sh` pinnt für Sie - auf den `sha-`-Tag des Commits, den es deployt - für genau so lange, wie der Deploy läuft. Pinnen Sie es aus dem Checkout dieses Releases: `git checkout v0.0.380` neben der Variablen. Das Image des Sandbox-Dienstes ist in der Compose-Datei gepinnt, auf die Version, die das Backend desselben Commits spricht. Ein älteres Backend aus einem neueren Checkout trifft also auf ein neueres `sandboxd` - und `latest` aus einem alten Checkout ist derselbe Versatz in die andere Richtung. `scripts/deploy.sh` checkt den Commit aus, den es deployt, also hält ein Deploy oder Rollback per Skript beides zusammen. Beide Pakete lassen sich anonym holen. Antwortet ein Pull mit `unauthorized`, wurde das Paket auf privat gestellt oder ein veraltetes `docker login ghcr.io` steht im Weg; keines von beidem kann ein Host von sich aus beheben. !!! danger "`backend/.env` hält den Schlüssel, der jede gespeicherte Zugangsinformation entpackt" `VAULT_MASTER_KEY` ist das, was die Provider-Keys, Bot-Token und MCP-Zugangsdaten einer Organisation lesbar macht. Ihn zu verlieren sperrt Sie nicht aus dem Produkt aus; es macht jedes Secret darin unwiederbringlich. Sichern Sie die Datei an einem Ort, den eine verlorene Festplatte nicht mitnimmt, und rotieren Sie mit [`agenticos cmd vault-rotate`](secrets.md#operations) statt durch Editieren. Zwei optionale Dinge, nach denen es nicht fragt, beide in dieser Datei: `SMTP_*`, ohne das sich Einladungen und Passwort-Resets nicht versenden lassen, und `LOGFIRE_TOKEN`, wohin die Traces der Agent-Runs gehen. ## Einen Reverse Proxy wählen { #choose-a-reverse-proxy } Irgendetwas muss TLS terminieren und die beiden Namen routen. Beide Optionen unten erreichen dieselben Container; wählen Sie danach, ob Sie bereits einen betreiben. !!! note "Komprimierung bleibt Sache der Anwendung" Die API komprimiert ihre eigenen Antworten mit gzip, der `/api/*`-Proxy der Konsole komprimiert für den Browser erneut, was die API komprimiert hat, und Next komprimiert seine Seiten. Der Proxy braucht also kein `gzip on`, und er darf das `Accept-Encoding` des Clients nicht leeren: das schaltet die Komprimierung nur ab. Eine Antwort, die bereits `Content-Encoding` trägt, reichen Nginx und Traefik unverändert durch. ### Option A: Traefik { #option-a-traefik } Der kürzere Weg, und der, den man auf einem Host mit bereits vorhandenem Traefik wählt: die Container tragen Labels, Traefik entdeckt sie, fordert das Zertifikat an und erneuert es. Nichts neu zu laden und keine zweite Konfigurationsdatei, die im Gleichschritt bleiben muss. Ist Traefik noch nicht vorhanden, liefert das Repository einen mit: `traefik/traefik.yml` und `docker-compose-traefik.yml`, das ist ein Entrypoint auf 443 mit einem Let's-Encrypt-Resolver und 80, das dorthin umleitet. ```bash docker network create traefik_webgateway docker compose --env-file backend/.env -f docker-compose-traefik.yml up -d ``` Wo Traefik **bereits** läuft, lassen Sie diese Dateien in Ruhe und zeigen mit `TRAEFIK_NETWORK` auf das Netzwerk, das er beobachtet. Die Overlays lesen diesen Namen, am bestehenden Proxy muss sich also nichts ändern. Bringen Sie den Stack dann mit `PROXY=traefik` hoch, was die beiden Overlay-Dateien hinzufügt, die die Labels tragen: ```bash make prod PROXY=traefik make prod-frontend PROXY=traefik ``` `server-init.sh` hat `PROXY=traefik` bereits in `backend/.env` geschrieben, und von dort liest `scripts/deploy.sh` es — spätere Deploys behalten also den Proxy bei, mit dem dieser Host eingerichtet wurde, und nicht den, den ein Skript annahm. !!! info "`exposedByDefault: false` leistet echte Arbeit" Es ist die eine Einstellung in `traefik/traefik.yml`, die es sich zu lesen lohnt, bevor Sie ihn betreiben. Nur `app` und `frontend` tragen `traefik.enable=true`, also sind Postgres, Redis, der Worker und der Sandbox-Daemon von nichts außerhalb des Hosts erreichbar — und das ist eine Eigenschaft davon, *nicht gelabelt zu sein*, sie überlebt also, dass jemand einen Dienst hinzufügt, ohne an den Proxy zu denken. !!! warning "Manche Request-Pfade und Query-Strings sind Zugangsdaten" Diese Pfadsegmente öffnen jedem, der sie liest, etwas: - der Schlüssel eines öffentlichen Links, in `/a/`, `/api/v1/public/artifacts/` und `/api/v1/artifact-embed/`, bis der Link rotiert wird; - eine signierte Inhaltsadresse, `/api/v1/artifact-content/`, für ihre Lebensdauer; - das Token einer geteilten Unterhaltung, in `/shared/` und `/api/v1/conversations/shared/`; - das Token eines Workflow-Formulars, in `/f/` und `/api/v1/public/workflow-forms/`; - das Token eines Webhook-Tests, in `/api/v1/workflow-webhook-tests/` und `/api/v1/workflows//webhook-test/`; - das Token einer Kanalverknüpfung, in `/link/` und `/api/v1/me/channel-link/`; - das Token einer Einladung, in `/invitations/`, `/api/v1/invitations/` und `/api/v1/invitations//accept`; - das Fortsetzungs-Token eines Workflow-Laufs, in `/api/v1/workflow-resume//`. Dasselbe gilt für diese Query-Parameter: - `visitor` und `token` am Socket des Agent-Widgets, `/api/v1/embed//ws`: der Schlüssel einer gehosteten Seite, mit dem sich die Unterhaltung des Besuchers fortsetzen lässt, und das signierte Besucher-Token des Kunden; - `code` und `state` bei der Rückkehr von einem Anmeldeanbieter, `/api/v1/oauth//callback`, und `code` auf `/auth/callback` der Konsole; - `invitation_handle` auf `/api/v1/oauth//login` und `/api/v1/auth/kerberos/login`, das die Einladung einlöst, für die es steht; - `token` auf `/reset-password`, `/auth/magic-link` und `/auth/email-change` der Konsole, den Links, die eine Passwort-Zurücksetzung, ein Anmeldelink und eine E-Mail-Änderung verschicken. Eine Anfrage, die von einer dieser Seiten ausgeht, kann deren Adresse im Header `Referer` mitführen. Die API ersetzt die API-Segmente und die Werte dieser Parameter durch `[REDACTED]` in ihrem eigenen Access-Log, in der Zeile, die uvicorn für jedes angenommene WebSocket schreibt, in jedem Log-Record und in Logfire-Spans, einschließlich der Konsolenausgabe, der Argumente des Endpunkts und der Kopie, die Logfire beim Start eines Spans anlegt. Andere Query-Parameter wie `q`, `skip`, `limit` und Ids bleiben erhalten. Traefik kann ein ganzes Feld behalten, verwerfen oder redigieren, aber keinen Teil davon umschreiben, deshalb entfernt `traefik/traefik.yml` `RequestPath`, das den Query-String enthält, aus dem Access-Log von Traefik. Nginx könnte einen Teil eines Werts umschreiben, doch `nginx/nginx.conf` lässt den Pfad, den Query-String und den `Referer` auf dieselbe Weise weg, mit dem Log-Format `agenticos_no_path`, das jeder Server-Block nennt, sodass die Liste der geheimen Pfade an einer Stelle gepflegt wird. Beide Logs enthalten weiterhin den Client, den Status, die Größe und die Dauer. Das Error-Log von Nginx schreibt die Request-Zeile und den `Referer` einer fehlgeschlagenen Anfrage, und keine Einstellung ändert das, behalten Sie das Error-Log also auf dem Host. Wenn Sie die Pfade im Access-Log brauchen, entfernen Sie diese Feldregel bzw. dieses Log-Format und behandeln Sie das Log als Secret: Behalten Sie es auf dem Host und schicken Sie es an keinen Log-Dienst. ### Option B: Nginx { #option-b-nginx } Für einen Host, auf dem Nginx bereits TLS terminiert, oder auf dem der Proxy gar nicht in Docker läuft. Der Stack veröffentlicht beide Ports auf `127.0.0.1`, und Nginx erreicht sie dort: ```bash make prod make prod-frontend ``` `nginx/nginx.conf` ist die Vorlage. Zwei Ersetzungen, bevor sie irgendetwas ausliefert: der `server_name` in jedem Block ist `${DOMAIN:-localhost}`, und Nginx expandiert das nicht — tragen Sie die beiden Hostnamen von Hand ein. Zertifikate zu beschaffen und zu erneuern ist Ihre Sache, und ebenso der Header `Strict-Transport-Security`, den das Backend bewusst dem überlässt, was TLS terminiert. Das Access-Log der Vorlage lässt den Request-Pfad, den Query-String und den `Referer` weg; *Manche Request-Pfade und Query-Strings sind Zugangsdaten* oben erklärt, warum, und was das Error-Log weiterhin schreibt. !!! warning "`BIND_HOST` ist eine Sicherheitseinstellung, keine Bequemlichkeit" Der Loopback-Standard ist das, was das Auth-Rate-Limit überhaupt bedeutsam macht. `RATE_LIMIT_TRUST_FORWARDED_FOR` weist die API an, einen Versuch der Adresse anzurechnen, die der Proxy weiterreicht — was immer die API also *am* Proxy *vorbei* erreichen kann, wählt die Adresse, der seine Versuche angerechnet werden. Setzen Sie `BIND_HOST=0.0.0.0` nur für einen Proxy auf einer anderen Maschine, und schirmen Sie den Port per Firewall auf ihn ein. !!! warning "Das Frontend ist ein eigenes Compose-Projekt" Compose benennt ein Projekt nach dem Verzeichnis, beide Stacks hießen also `agenticos` — und das Hochfahren des Frontends meldete dann die fünf Backend-Container als **Waisen**, mit Composes eigenem Vorschlag, das Kommando erneut mit `--remove-orphans` auszuführen. Diesem Rat zu folgen stoppt die API, die Datenbank, Redis und den Worker. Die `make`-Targets und `scripts/deploy.sh` übergeben `-p agenticos-frontend`, die Warnung ist damit weg. Die Compose-Dateien legen auch keine Containernamen fest - jedes Projekt benennt seine eigenen, zwei Stacks auf einem Host können einander also nicht die Container wegnehmen, und `deploy.sh` wartet auf die Dienste `app` und `frontend` statt auf einen Namen. Ein Deployment, das älter ist als beide Korrekturen, wird bei seinem nächsten `up` unter den neuen Namen neu erstellt; von Hand muss nichts entfernt werden. Die beiden Projekte treffen sich weiterhin auf einem Netzwerk mit festem Namen - `agenticos_edge` in Produktion, `agenticos_backend` auf dem Dev-Server - weil das Frontend ihm als externem Netzwerk beitritt. Ein Host, auf dem **zwei** AgenticOS-Stacks laufen, setzt `AGENTICOS_EDGE_NETWORK` und `AGENTICOS_DATA_NETWORK` (oder `AGENTICOS_NETWORK`) in der `backend/.env` jedes Stacks unterschiedlich; sonst lösen `db`, `redis` und `app` beider Stacks auf einer Bridge auf, und eine Anfrage kann die Datenbank des Nachbarn erreichen. ## Starten und den ersten Zugang anlegen { #start-it-and-create-the-first-account } `make prod` holt die Images, startet den Stack und führt die Migrationen aus - letztere als `migrate`-Dienst, auf den die API wartet, ein `docker compose up -d` von Hand auf denselben Dateien tut also dasselbe. Der erste Pull umfasst rund 2 GB; ein späterer sind die Layer, die sich geändert haben. Legen Sie dann eine Organisation, einen Owner und einen funktionierenden Agent an: ```bash docker compose --env-file backend/.env -f docker-compose-prod.yml \ exec -T app agenticos cmd bootstrap \ --email you@example.com --password 'a real password' \ --org 'Your Company' --provider anthropic --api-key sk-ant-... ``` Der Provider-Key an dieser Stelle ist das, worauf die zwei Start-Agents laufen. Ohne ihn werden sie angelegt und können nicht antworten; jeder andere Provider wird im Produkt hinzugefügt, pro Organisation, aus dem Vault. !!! tip "Prüfen Sie es von außen, nicht vom Host aus" ```bash curl -fsS https://api.example.com/api/v1/health curl -fsSo /dev/null -w '%{http_code}\n' https://app.example.com ``` Ein Stack, der auf dem Host gesund und aus dem Internet unerreichbar ist, ist DNS, die Firewall oder das Zertifikat — drei Dinge, die ein Health Check innerhalb des Hosts nicht sehen kann. Dann, einmal, von Hand: melden Sie sich an, laden Sie jemanden ein (was `SMTP_*` nachweist) und schicken Sie AgenticOS eine Nachricht (was den Provider-Key und den WebSocket nachweist). Jedes davon übt einen Pfad aus, den hier sonst nichts prüft. !!! info "Die Security-Header kommen aus dem Backend, jeder Proxy ist damit abgedeckt" Eine Content-Security-Policy, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy` und `Permissions-Policy` werden auf jeder Antwort gesetzt — einschließlich der 500 für eine unbehandelte Exception, die außerhalb des Middleware-Stacks gebaut wird und sie sich selbst aufprägt. Nur die interaktive API-Dokumentation verzichtet auf die CSP, weil Swagger Assets lädt, die eine strenge Policy verbietet. **HSTS ist bewusst dem Proxy überlassen**, denn dort terminiert TLS. Ein Proxy, der eine eigene CSP setzt, sollte mindestens so streng sein wie diese. ### Dienste im eigenen Netzwerk { #services-on-your-own-network } Eine von einem Mitglied konfigurierte Anfrage - ein Workflow-HTTP-Schritt, eine Git-, Website- oder S3-Quelle, ein A2A-Agent oder eine MCP-Connection - wird abgelehnt, wenn ihr Host auf eine private Adresse auflöst. Ein Wiki, ein Git-Server oder ein MCP-Server in Ihrem Netz ist daher standardmäßig nicht erreichbar. Um einen zu öffnen, tragen Sie ihn in `OUTBOUND_PRIVATE_ALLOWLIST` in `backend/.env` ein, als Host, `.suffix`, Adresse oder CIDR-Bereich, und starten Sie API und Worker neu. Die Zeile dieser Einstellung in der [Konfigurationsreferenz](configuration.md#project-settings) nennt die Formen, was unabhängig von Ihrer Liste abgelehnt wird und was dann jedes Konto erreicht. Listen Sie nur Dienste, die ihre eigenen Zugangsdaten prüfen. `agenticos cmd doctor` meldet die Liste als Zeile `private network`. Er schlägt fehl, wenn ein gelisteter Bereich die Datenbank oder Redis abdeckt, und warnt vor einem Bereich, der größer ist als /16 (IPv4) oder /48 (IPv6). ### Die Sandbox einschalten { #turning-the-sandbox-on } Der Dienst, der den Code eines Agents ausführt, liegt hinter einem Compose-Profil, denn er ist der eine Container, der den Docker-Socket hält, und den auf einem gemeinsam genutzten Host einzuhängen sollte eine Entscheidung sein statt eine Voreinstellung. Drei Dinge, einmalig: ```bash make sandbox-token # writes SANDBOXD_TOKEN to backend/.env sudo mkdir -p /var/lib/agenticos/sandbox-workspaces sudo chown 10001:10001 /var/lib/agenticos/sandbox-workspaces ``` Ein Deploy bringt sie dann hoch: `scripts/deploy.sh` übergibt `--profile sandbox`, wenn `SANDBOXD_TOKEN` in `backend/.env` einen Wert hat, der Host selbst sagt also, ob er eine betreibt. Wie `make` und Quickstart exportiert es `DOCKER_GID` in `group_add` der Sandbox: die Socket-Gruppe unter Linux (Symlinks werden verfolgt) oder `0` bei Docker Desktop, wo der eingebundene Socket der VM gehört. Ein explizites `DOCKER_GID` in der Umgebung oder auf der `make`-Befehlszeile hat Vorrang. Sonst wird nichts in `.env` gebraucht: das Backend erreicht den Daemon über eine Sandbox-*Connection*, die jemand in der Konsole anlegt, und `http://sandboxd:8080` wird als die eigene dieses Deployments erkannt. !!! warning "Ein Profil, von dem Compose nichts weiß, ist ein Dienst, den Compose stoppt" Ein `up -d` auf demselben Projekt ohne `--profile sandbox` lässt die Sandbox nicht in Ruhe — es stoppt sie. Einem Host, der sie von Hand gestartet hatte, wurde sie also vom nächsten Deploy weggenommen, wobei die Codeausführung eines Agents aus Gründen fehlschlug, die weit weg vom auslösenden Deploy lagen (#1506). Deshalb liest das Skript den Token, statt ein Flag entgegenzunehmen. ## Eine Änderung deployen { #deploying-a-change } ### Nativer Speicher und Kontinuität von Routinen { #native-memory-upgrade } Erstelle vor dem Upgrade [Sicherungen von Datenbank, Medien und Vault-Schlüsseln](#backups) und koordiniere API- und Worker-Rollout. Nutze den normalen Migrationsschritt des Deployments; kopiere keine Zeilen und lege keine Ersatzzeitpläne manuell an. Stoppe vor `0285_member_proactivity.py` API und Sweep-Erzeuger, lasse aktive proaktive Jobs auslaufen und stoppe dann die Worker. Die Migration verweigert laufende Watch- oder nächtliche Modelljobs und ergänzt wartende Jobs um einen gemeinsamen Parallelitätsschlüssel pro Person. Aktualisiere API und Worker gemeinsam, damit neue Jobs denselben Schlüssel nutzen; eine Watch und ein nächtlicher Durchlauf derselben Person dürfen nicht unabhängig dasselbe Restbudget ausgeben. - `0248_drop_memory_mem0.py` stoppt das Upgrade, solange `memory_mem0` in einem gespeicherten Entwurf oder veröffentlichten Spec verbleibt. Inventarisiere vor dem erneuten Versuch alle Endpunkte und Vault-Zugangsdatenzuordnungen aus der Sicherung, lösche die externen Erinnerungen und bestätige den Abschluss bei jedem Anbieter. Entferne erst danach diese Bindungen ausdrücklich aus `agents.draft_spec` und `agent_versions.spec`. Ein Export allein ist keine Löschung. Bewahre die Löschbelege auf, prüfe ungenutzte Vault-Schlüssel und widerrufe ausgemusterte Zugangsdaten. Die Migration führt keine Remote-Anfrage aus und setzt keine erfolgreiche Löschung voraus. Hat ein früherer Entwicklungsstand die Bindungen bereits entfernt, stelle das Inventar aus der Sicherung vor dem Upgrade wieder her und führe dieselbe Löschung durch; die native Personen-/Agentenlöschung erreicht diese externen Daten nicht. - `0251_primary_conversations.py` und `0258_primary_channel_history.py` richten primäre private Chats ein. Bestehende Zuordnungen haben Vorrang. Nur nachweislich privater Verlauf eines verknüpften Kanals wird einmalig übernommen; Projekt-, Gruppen- und unklare Verläufe bleiben getrennt. Wiederhole die Übernahme nicht, nachdem jemand seinen primären Chat geleert hat. - Bestehende Routinen-IDs, Zeitpläne und Logs bleiben erhalten. `0255_trigger_delivery.py` setzt alte Routinen auf `run_log`; neue verwenden standardmäßig den primären Chat oder ihre vertrauenswürdige Quellunterhaltung. Prüfe Ziel, handelnde Person, Ende und Budget unter **Routinen**, statt Kopien anzulegen. - `0259_trigger_version_pin.py` erlaubt unveränderliche Versionspins. Ein Pin und eine benannte Umgebung schließen sich aus. Entferne einen Pin vor dem Löschen seiner Version. Ein geparkter Run behält seine zugelassene Version; `0260_routine_run_origin.py` erhält seine Herkunft, damit das Löschen der Routine ihre Berechtigungsprüfung nicht aufhebt. - `0261_watch_attempt_status.py` beginnt die explizite Versuchserfassung ohne erfundene ältere Erfolgs-/Fehlerhistorie. Nicht verfügbare oder unvollständige Prüfungen verschieben den Erfolgszeitpunkt nicht. Prüfe bestehende Beobachtungen und ihren Kontozugriff: alte Zeilen beweisen nicht, wie die ursprüngliche Einwilligung erteilt wurde. Wähle vor dem Einschalten der Speicherpflege ausdrücklich ein Modell; das Tagesbriefing behält die bisherigen Modell-/Budgeteinstellungen der Organisation. Seit `0262_watch_agent_sources.py` verwenden Beobachtungen den ausgewählten Agenten und behalten die genehmigten Quellen im Run. Bestehende Beobachtungen erfordern eine Agentenauswahl und Quellenfreigabe; IDs und Funde bleiben erhalten. Prüfe [Governance](governance.md#proactive-work) und die [Kanaleinrichtung](channels.md#sms), einschließlich `PUBLIC_BASE_URL` und abgelaufener SMS-Verknüpfungen. Prüfe in einer Testorganisation einen privaten Chat, Aktivität/Zustellung einer Routine, Pause/Ablauf und eine Beobachtung mit nicht verfügbarer Quelle vor einem breiten Rollout. ### Die Grenze des Verlaufsformats in 1.25.0 { #retry-history-upgrade } Version 1.25.0 übernimmt die Tool-Ergebnisse `retried` und separate Nachrichten `retry-feedback` aus Pydantic AI 2.61. Vorhandene Verläufe und Run-Checkpoints werden ohne Datenbankmigration oder erneutes Veröffentlichen der Agenten gelesen. Verläufe mit diesen Teilen benötigen den neuen Leser: Ein älteres Backend kann diese Nachrichtenformen nicht wiedergeben. Aktualisieren Sie alle API- und Worker-Prozesse zusammen. Lassen Sie alte Prozesse auslaufen oder stoppen Sie sie, bevor neue Arbeit annehmen. Mischen Sie keine älteren und neueren Backend-Replikate an derselben Datenbank. Sobald eines der neuen Retry-Formate gespeichert wurde, wird ein reiner Code-Rollback über diese Grenze nicht unterstützt; der neue Leser muss bleiben oder ein Datenbank-Snapshot von vor dem Upgrade wiederhergestellt werden. ### Von Hand { #by-hand } ```bash remote=$(ssh you@your-host 'mktemp -t agenticos-deploy.XXXXXX') ssh you@your-host "cat > $remote" < scripts/deploy.sh ssh you@your-host "trap 'rm -f $remote' EXIT; bash $remote " ``` `scripts/deploy.sh` holt diesen Commit, wartet auf die Images, die die CI dafür veröffentlicht hat (`sha-`, meist schon da), holt sie, startet neu und wartet darauf, dass beide Container sich als gesund melden, bevor es mit einem Wert ungleich null zurückkehrt oder eben nicht. Es nimmt einen **Commit** statt eines Branches entgegen, das Deployte ist also das Geprüfte und nicht das, wohin `main` seither gewandert ist - und was läuft, ist Byte für Byte das, was die CI gebaut hat, auf einem Host, der die Toolchain nie braucht. !!! warning "Kopieren Sie es auf den Host und führen Sie es dann aus — leiten Sie es nicht in `bash -s`" Unter `bash -s` ist das Skript die Standardeingabe der Shell selbst, und das erste Kommando darin, das stdin liest, verbraucht den Rest. `docker compose exec` reicht stdin auch mit `-T` an den Container weiter, die Migration hat also alles unter sich aufgefressen, bash erreichte EOF, und der Deploy endete mit **0**, ohne jemals das Frontend gebaut oder auf irgendeinen Container gewartet zu haben. Die Website war unten und der Deploy war grün ([#1488](https://github.com/vstorm-co/agenticos/issues/1488)). Zwei Verbindungen statt einer sind das, was der Prozedur die Fähigkeit nimmt, sich selbst abzuschneiden. Es ist nicht ausfallfrei. Compose erstellt die Container neu, deren Image sich geändert hat, die Website ist also für die wenigen Sekunden, die das dauert, nicht verfügbar. ### Aufbewahrung von Release-Images { #release-image-retention } Vor dem Herunterladen entfernt das Skript alte, ungenutzte Release-Images nur aus `ghcr.io/vstorm-co/agenticos-backend`, `agenticos-frontend` und `agenticos-browser`. Es behält je Repository zwei zusätzliche ungenutzte Image-IDs, die neuesten zuerst. Mehrere SHA- oder Versions-Tags derselben ID zählen als ein Image. Nur `sha-` und numerische Versions-Tags kommen infrage. Alle Aliasse einer entfernbaren ID werden gemeinsam und ohne `--force` entfernt. `--no-prune` verhindert auch das implizite Löschen unmarkierter Eltern-Images. Jeder laufende oder gestoppte Container auf dem Host schützt sein Image, auch in anderen Compose-Projekten. Das Ziel-Image, ein veränderlicher oder manueller Tag sowie ein Tag oder Digest in einem anderen Repository schützen ebenfalls die gesamte ID. Sandbox-/Laufzeit-Images, Images ohne Tags und Anwendungs-Volumes bleiben unberührt. Diese Schutzregeln können mehr Images als das Kontingent für ungenutzte Images erhalten. Der Hash des Deployment-Verzeichnisses trennt die Markierungen mehrerer Stacks auf einem Host. Lokale Tags `deploy--current` und `deploy--rollback` erhalten das letzte gesunde Image jeder Komponente und dessen Vorgänger nach dem Austausch der Container. Beim ersten Einsatz wird `deploy--current` aus den gesunden Containern dieses Stacks ermittelt, nicht aus dem Checkout; unterschiedliche gesunde IDs derselben Komponente verweigern das Deployment. Die Markierungen wechseln erst nach erfolgreichen Healthchecks der neuen Dienste. Fehlgeschlagene Deployments behalten die gespeicherten Markierungen; ein erneutes Deployment desselben Images ersetzt seinen Rollback nicht. Rollbacks erfolgen weiterhin über einen genehmigten Commit-SHA: Diese lokalen Tags sind Aufbewahrungsmarkierungen, keine Registry-Releases. Das Skript verlangt mindestens **5 GiB freien Speicher** im Docker-Dateisystem vor jedem Pull und nach der optionalen Vorbereitung von Laufzeitumgebungen. Es prüft `DockerRootDir` und, falls vorhanden, den Standardspeicher `/var/lib/containerd`. Dies ist ein Mindestwert, keine Reservierung oder Schätzung der Image-Größe; benutzerdefinierte containerd-Speicher müssen separat überwacht werden. Backend- und Frontend-Images werden heruntergeladen, bevor einer der Dienste neu startet. Bei zu wenig Speicher nennt die Ablehnung das Dateisystem und den verfügbaren Platz. Speicher außerhalb der Schutzregeln freigeben oder erweitern, dann das genehmigte Deployment erneut starten. Diese Richtlinie niemals durch ein globales `docker image prune -a` umsetzen. Bei Docker Desktop liegt das Speicherverzeichnis in der Linux-VM. Das Skript misst deren freien Speicher mit einem temporären, schreibgeschützten und vom Netz getrennten Container aus einem bereits lokal vorhandenen Backend-Image, vorzugsweise dem letzten gesunden. Die Prüfung darf keine Images herunterladen und der Container wird automatisch entfernt. Zusätzlich wird das Host-Dateisystem des Deployment-Verzeichnisses geprüft; das Host-Dateisystem eines eigenen VM-Datenträgers muss separat überwacht werden. Dieses Update-Verfahren benötigt ein lokal vorhandenes Backend-Image; zuerst die normale Erstinstallation abschließen. ### Aus GitHub, mit einer Freigabe { #from-github-with-an-approval } `.github/workflows/deploy.yml` bietet jedes veröffentlichte stabile Release zum Deployment an und wartet darauf, dass jemand ihn freigibt. Dieses Tor ist **eine Repository-Einstellung, kein Schritt in der Datei** — ohne es deployt der Workflow jedes veröffentlichte stabile Release unbeaufsichtigt. Einmal einrichten: 1. **Settings → Environments → New environment**, benannt `production`. 2. **Required reviewers** ankreuzen und hinzufügen, wer freigeben darf. Das ist das Tor. 3. Die Variablen der Environment hinzufügen: `SITE_URL`, `API_URL` und `APP_DIR`, falls der Checkout nicht unter `/opt/agenticos` liegt. 4. Die Secrets unten hinzufügen. !!! warning "Brechen Sie einen Deploy ab, den Sie nicht freigeben wollen" Jeder Lauf teilt sich die Concurrency-Gruppe `deploy-production`, und ein Lauf, der am Freigabe-Tor sitzt, hält sie. Er läuft nicht von selbst ab — GitHub bricht einen unbearbeiteten nach 30 Tagen ab — bis also jemand ihn freigibt oder abbricht, stauen sich spätere Releases hinter einer Entscheidung, die niemand treffen wird, und der Server betreibt weiter das, was zuletzt deployt wurde. Ein Deploy, gegen den Sie sich entschieden haben, wird also abgebrochen und nicht stehen gelassen. Einer, der auf einem überholten Commit wartete, hat hier drei spätere Läufe blockiert, bevor jemand die Warteschlange statt der Läufe bemerkte. `deploy-queue.yml` ist das, was passiert, wenn er trotzdem stehen bleibt: alle sechs Stunden bricht es einen Deploy-Lauf ab, der länger als zwölf auf eine Freigabe gewartet hat, und legt ein Issue an, das den Lauf benennt und sagt, wie weit das neueste stabile Release vom letzten erfolgreichen Deploy abgedriftet ist. Es gibt nichts frei und deployt nichts — es leert die Warteschlange, damit das nächste veröffentlichte Release das Tor erreicht, und sagt, dass es das getan hat. Von Hand mit `gh workflow run deploy-queue.yml`. !!! danger "Eine Freigabe aus der Warteschlange der Environment gibt den **ältesten** Lauf frei" Also den veralteten. Eine späte Freigabe dort deployt den Commit von dem Moment, in dem die Warteschlange stecken blieb, und nicht den gerade gepushten — so kam ein zwei Wochen alter Commit auf diesen Server. Geben Sie von der Seite des Laufs frei, den Sie meinen, oder starten Sie mit `gh workflow run deploy.yml --ref main -f ref=v1.34.3` einen neuen und geben Sie den frei. | Secret | Was | |---|---| | `DEPLOY_HOST` | Die Adresse des Hosts | | `DEPLOY_USER` | Das Konto, dem der Checkout gehört | | `DEPLOY_SSH_KEY` | Ein privater Schlüssel, dessen öffentliche Hälfte in den `authorized_keys` dieses Kontos liegt | | `DEPLOY_KNOWN_HOSTS` | `ssh-keyscan your-host`, ausgeführt von einem Ort, dem Sie vertrauen | Erzeugen Sie den Schlüssel dafür und für nichts sonst: ```bash ssh-keygen -t ed25519 -N '' -C 'github-actions-deploy' -f deploy_key ssh-copy-id -f -i deploy_key.pub you@your-host ssh-keyscan your-host # → DEPLOY_KNOWN_HOSTS cat deploy_key # → DEPLOY_SSH_KEY, then delete it locally ``` !!! note "Der Host-Key ist ein Secret und kein `ssh-keyscan` zur Deploy-Zeit" Zur Deploy-Zeit zu scannen vertraut allem, was unter dieser Adresse antwortet, und genau das ist es, was ein Host-Key verhindern soll. Scannen Sie einmal, von einem Ort, dem Sie vertrauen, und speichern Sie die Antwort. Es erscheint dann ein Lauf mit **Review deployments**; ihn freizugeben startet den Job. `workflow_dispatch` führt denselben Job gegen eine von Ihnen benannte Ref aus, so wird ein Rollback gemacht, und er durchläuft dieselbe Freigabe. ## Backups { #backups } Ein Volume zählt, und welches, ist nicht offensichtlich: | Volume | Enthält | Backup | |---|---|---| | `postgres_data` | alles — Agents, Unterhaltungen, versiegelte Zugangsdaten | **ja** | | `media_data` | hochgeladene Dateien, vor der Ingestion | ja | Der Cache hat überhaupt kein Volume mehr. Rate-Limit-Buckets, Dedupe-Claims der Kanäle und Mitgliedschaftsantworten tragen alle eine TTL und bauen sich selbst wieder auf, also läuft Valkey mit `--save ''` und startet nach einem Neustart leer. Ein Host, auf dem ein früheres Release lief, trägt noch ein Volume `agenticos_redis_data`, das nichts mehr mountet; `docker volume rm agenticos_redis_data` entfernt es. ```bash docker compose --env-file backend/.env -f docker-compose-prod.yml exec -T db \ sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' > "agenticos-$(date +%F).dump" ``` Die Bezeichner kommen aus der Umgebung des Containers selbst, statt ausgeschrieben zu werden, denn beide sind Einstellungen: ein Deployment, das eine davon geändert hat, bekäme sonst eine leere Datei und einen Fehler, den im Vorbeigehen niemand liest. !!! danger "Ein Datenbank-Backup ohne `backend/.env` ist kein Backup" Die Zugangsdaten darin sind mit `VAULT_MASTER_KEY` versiegelt. Neben einem anderen Schlüssel wiederhergestellt, ist jeder Provider-Key, jeder Bot-Token und jede MCP-Zugangsinformation im Dump unlesbar — und das Produkt wird Ihnen das eine Ablehnung nach der anderen mitteilen. ## Zurückrollen { #rolling-back } | | Wie | |---|---| | **Code** | Den vorherigen Commit deployen: `workflow_dispatch` mit seinem SHA, oder `scripts/deploy.sh`. Die Images liegen noch in der Registry, das ist also ein Pull und kein Build. Ein Commit ohne `sha-`-Images - älter als `images.yml`, oder sein Lauf ging verloren - wird zuerst mit `gh workflow run images.yml --ref main -f sha=` veröffentlicht; der Deploy nennt dieses Kommando, wenn er das Warten aufgibt | | **Schema** | `agenticos db downgrade --revision=-1`, dann den passenden Code deployen | | **Daten** | Den Dump per `pg_restore` einspielen, dann die Migration prüfen, die der Code erwartet | Code **über eine Migration hinweg** zurückzurollen **ist eine Entscheidung, kein Kommando**. Der alte Code trifft auf ein Schema, das er nie gesehen hat; ob das funktioniert, hängt von der Migration ab. Lesen Sie sie, bevor Sie etwas annehmen. ## Zusammenfassung { #recap } - **Ein Host, Compose, ein Proxy davor.** Sieben Container - einer davon führt die Migrationen aus und beendet sich - zwei davon zustandsbehaftet, jeder einzelne geholt - auf dem Host wird nichts gebaut. Pinnen Sie `AGENTICOS_VERSION`. - **`UVICORN_WORKERS` entscheidet, was der Host kostet.** 460 MiB pro Worker, nichts geteilt. Zwei für ein Team, vier für echten Verkehr. - **DNS vor allem anderen.** Es wird kein Zertifikat ausgestellt, bis die Namen auf den Host auflösen. - **Die Freigabe ist eine Repository-Einstellung**, keine Zeile im Workflow. Ohne Required Reviewers auf der Environment `production` deployt sich jeder Merge selbst. - **Sichern Sie `postgres_data` und `backend/.env` zusammen.** Das eine ohne das andere ist keine Wiederherstellung.