docs/deploy.de.md
Rama main · réplica de lectura del repositorio original.
---
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-<short>`. 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-<short>`-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=<commit>` -
und er veröffentlicht den `sha-<short>`-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/<key>`,
`/api/v1/public/artifacts/<key>` und `/api/v1/artifact-embed/<key>`, bis
der Link rotiert wird;
- eine signierte Inhaltsadresse, `/api/v1/artifact-content/<token>`, für
ihre Lebensdauer;
- das Token einer geteilten Unterhaltung, in `/shared/<token>` und
`/api/v1/conversations/shared/<token>`;
- das Token eines Workflow-Formulars, in `/f/<token>` und
`/api/v1/public/workflow-forms/<token>`;
- das Token eines Webhook-Tests, in `/api/v1/workflow-webhook-tests/<token>`
und `/api/v1/workflows/<id>/webhook-test/<token>`;
- das Token einer Kanalverknüpfung, in `/link/<token>` und
`/api/v1/me/channel-link/<token>`;
- das Token einer Einladung, in `/invitations/<token>`,
`/api/v1/invitations/<token>` und `/api/v1/invitations/<token>/accept`;
- das Fortsetzungs-Token eines Workflow-Laufs, in
`/api/v1/workflow-resume/<run_id>/<token>`.
Dasselbe gilt für diese Query-Parameter:
- `visitor` und `token` am Socket des Agent-Widgets,
`/api/v1/embed/<key>/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/<provider>/callback`, und `code` auf `/auth/callback` der
Konsole;
- `invitation_handle` auf `/api/v1/oauth/<provider>/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 <commit-sha>"
```
`scripts/deploy.sh` holt diesen Commit, wartet auf die Images, die die CI dafür
veröffentlicht hat (`sha-<short>`, 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-<hex>` 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-<stack-hash>-current` und `deploy-<stack-hash>-rollback` erhalten das letzte gesunde
Image jeder Komponente und dessen Vorgänger nach dem Austausch der Container.
Beim ersten Einsatz wird `deploy-<stack-hash>-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-<short>`-Images - älter als `images.yml`, oder sein Lauf ging verloren - wird zuerst mit `gh workflow run images.yml --ref main -f sha=<commit>` 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.