docs/deploy.pl.md
Rama main · réplica de lectura del repositorio original.
---
source_sha: "0d080c65b077"
---
# Wdrożenie na serwer { #deploy-to-a-server }
Jeden host, Docker Compose i reverse proxy pozostają dostarczaną ścieżką dla
pojedynczego serwera. Dla klastrów użyj utrzymywanego [charta Helm Kubernetes](kubernetes.md),
z zewnętrznymi usługami danych i blokującym zadaniem migracji. Konfiguracja OpenShift
jest propozycją i nie została jeszcze zweryfikowana. Nie ma jednoklikowych quickstartów PaaS.
Poza jeden host - zarządzany Postgres, kilka hostów API, kilka wdrożeń - prowadzi
[Skalowanie poza jeden host](scaling.md).
!!! tip "Przeczytaj najpierw [listę kontrolną produkcji](configuration.md#production-checklist)"
Dziewięć ustawień ma domyślne wartości, które są w porządku na laptopie i złe
na hoście, do którego ktoś inny może sięgnąć. `scripts/server-init.sh` poniżej
generuje wszystkie dziewięć, więc lista kontrolna jest tym, co sprawdzasz
potem, a nie tym, co wpisujesz.
## Czego potrzebujesz { #what-you-need }
| | |
|---|---|
| **Host** | 4 vCPU i 8 GB RAM to uruchamia. Zobacz [dobór rozmiaru](#sizing-the-host) |
| **Silnik kontenerów** | Engine 24+ z wtyczką Compose (2.24 lub nowszą) i twój użytkownik w grupie `docker`. Nic nie jest budowane na hoście: obrazy są ściągane z GHCR. Alternatywnie użyj [ręcznej ścieżki Podmana](#podman-support). |
| **Dwie nazwy hostów** | jedna dla witryny, jedna dla API — zobacz [dlaczego dwie](#why-two-hostnames) |
| **Reverse proxy** | [Traefik](#option-a-traefik) albo [Nginx](#option-b-nginx). To on terminuje TLS |
| **Bez klucza providera** | modele czatowe i embedy są kluczowane per organizacja, w vaulcie produktu. Środowisko nie trzyma poświadczeń ani dla jednych, ani dla drugich — zobacz [Ochrona danych](data-protection.md#nothing-leaves-by-default) |
Host potrzebuje też otwartych portów 80 i 443, i niczego więcej. Postgres i Redis
nie są publikowane na żadnym interfejsie, a worker niczego nie serwuje.
### Dlaczego dwie nazwy hostów { #why-two-hostnames }
Przeglądarka rozmawia z obiema. Większość wywołań idzie przez własne
serwerowe route'y frontendu, ale WebSocket czatu łączy się z API bezpośrednio,
więc API potrzebuje nazwy, którą przeglądarka potrafi rozwiązać, i własnego
certyfikatu.
`app.example.com` i `api.example.com` to ten kształt. Mogą to być dowolne dwie
nazwy; czym być nie mogą, to jedną nazwą z prefiksem ścieżki, bo ciasteczka API i
ciasteczka witryny są ograniczone do hosta.
### Obsługa Podmana { #podman-support }
**Podman jest wspierany w ręcznym wdrożeniu z Docker Compose jako providerem.**
Testy wykonano na Podmanie 6.1.4 i Docker Compose v2.40.3, Fedora CoreOS 44
(VM Podmana na macOS, ARM64, cgroups v2, SELinux enforcing). Dostarczany plik
wymaga Compose 2.24 lub nowszego.
**Nie używaj `podman-compose` 1.6.0.** Uruchomił stack i agent odpowiedział,
ale kontrolowana nieudana migracja nie zatrzymała startu zależnej aplikacji.
Docker Compose poprawnie odmówił startu. Providerzy mają osobne numery wersji;
`podman-compose` 1.6.0 nie oznacza Docker Compose 1.6.0.
Quickstart pozostaje przeznaczony dla Dockera i odsyła wykrytego Podmana do
tej ręcznej ścieżki, także z `--no-sandbox`. W skonfigurowanym katalogu głównym
repozytorium wskaż rzeczywisty plik wykonywalny Docker Compose (osobny program
lub plugin CLI):
```bash
export PODMAN_COMPOSE_PROVIDER=/absolute/path/to/docker-compose
podman compose version
podman compose -f docker-compose.yml up -d
```
Sprawdź, czy `podman compose version` identyfikuje Docker Compose. Jawne `-f`
pomija lokalny override budujący ze źródeł: ta instalacja używa opublikowanych
obrazów. Po starcie usług wykonaj bootstrap organizacji i modelu zgodnie z
[Instalacją](install.md).
**Opcjonalny sandbox.** `pydantic-ai-backend` 0.2.39 działał z socketami Podmana
rootful i rootless. Nie wymaga nowego backendu ani wydania. Użyj pliku
`docker-compose-podman.yml`, aby dostosować montowanie socketu i workspace;
przygotuj uprawnienia przed włączeniem profilu.
Na natywnym Linuksie z rootless Podmanem:
```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
```
Dla rootful Linuksa włącz systemowy `podman.socket`, ustaw
`SANDBOX_DOCKER_SOCKET=/run/podman/podman.sock` i przygotuj workspace przez
`sudo install -d -o 10001 -g 10001 "$SANDBOX_WORKSPACE_ROOT"`. Uruchom Compose
przeciw temu silnikowi rootful. Grupa socketu to zwykle 0; przy niestandardowym
sockecie rootful podaj jego rzeczywistą grupę w `DOCKER_GID`.
Na rootless VM Podmana w macOS oba źródła bind mountów należą do linuksowej VM,
a nie macOS. Przygotuj je wewnątrz VM:
```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
```
Zachowaj istniejące klucze wdrożenia. Ustaw niepusty `SANDBOXD_TOKEN` i
`SANDBOX_URL=http://sandboxd:8080` w pliku env czytanym przez stack, zgodnie z
[konfiguracją sandboxa](sandbox.md). Następnie uruchom oba pliki:
```bash
podman compose -f docker-compose.yml -f docker-compose-podman.yml \
--profile sandbox up -d
```
Override wyłącza etykietowanie SELinux tylko dla `sandboxd`, który już ma
socket silnika. Udostępnia etykietę workspace przez `:z`, ponieważ montują go
również sąsiednie sandboxy. Kontenery sandboxów zachowują własne etykiety i
`no-new-privileges`; nie zmieniaj etykiety socketu ani nie wyłączaj globalnie
SELinux. W rootless właściciel socketu mapuje się na grupę 0 w user namespace.
Na serwerze użyj trwałej ścieżki workspace: `/tmp` to tylko lokalny przykład.
Zachowaj ustawienia socketu, workspace i grupy w środowisku operatora dla
kolejnych poleceń Compose. Hostowe `DOCKER_HOST` nie zmienia socketu montowanego
wewnątrz `sandboxd`.
Zweryfikowano run agenta przez HTTP i zapis ukończenia, pliki workspace,
niezerowe kody wyjścia, timeouty, egzekwowane limity RAM/CPU/PID i budowanie
obrazów runtime. Dostarczany obraz workbench czytał/zapisywał XLSX, renderował
wykres oraz uruchamiał Node i LibreOffice. Model był deterministycznym serwerem
testowym zgodnym z OpenAI; nie testowano runtime desktop ani każdej dystrybucji
Linuksa. [Raport testów](https://github.com/vstorm-co/agenticos/blob/main/docs/audits/podman-2026-11-03.md)
zawiera wersje, zakres i reprodukcję błędnej obsługi migracji.
## Dobór rozmiaru hosta { #sizing-the-host }
Zmierzone na bezczynnym wdrożeniu, nie oszacowane:
| | w spoczynku | sufit |
|---|---|---|
| `app` (2 workery uvicorna) | ~1,0 GB | 2,5 GB przy domyślnych 4 workerach |
| `db` | ~1,3 GB przy strojeniu poniżej | 2 GB |
| `worker` (jeden proces potomny ingestu) | 387 MiB | 2,5 GB (`WORKER_MEMORY_LIMIT`) |
| `frontend` | ~300 MB | 1 GB |
| `redis` | 9 MiB | 512 MB |
Liczbą, która decyduje o hoście, jest **`UVICORN_WORKERS`**. Każdy worker to
osobny proces importujący całą aplikację — 460 MiB, spawnowany, a nie forkowany,
więc nic nie jest współdzielone. Czterech z nich to 1,9 GB, zanim dotrze
jakiekolwiek żądanie.
Dwóch workerów wystarczy zespołowi dziesięciu osób i wciąż zostawia jednego
obsługującego ruch, kiedy
[watchdog](configuration.md#a-worker-whose-event-loop-has-stopped-turning)
zastępuje zaklinowane rodzeństwo. Jeden worker to ustawienie, którego należy
unikać: zablokowana pętla zdarzeń jest wtedy całym wdrożeniem, dopóki worker się
nie zabije.
Host, któremu brakuje CPU, daje inny objaw: wpisy `Child process [N] died` w logu
`app` bez werdyktu watchdoga nad nimi, a każda strona działa wolno. To workery,
które supervisor uvicorna zabił za spóźnioną odpowiedź na ping, a nie workery,
które padły. Czeka on
[`UVICORN_WORKER_HEALTHCHECK_TIMEOUT`](configuration.md#a-worker-whose-event-loop-has-stopped-turning)
sekund, domyślnie 60. Zanim to podniesiesz, sprawdź CPU steal na hoście.
Dwa ustawienia sprawiają, że wolne wdrożenie da się zdiagnozować:
- **Każdy log ma limit.** Każda usługa w produkcyjnych plikach compose zapisuje
najwyżej pięć plików po 20 MB. Domyślnie Docker trzyma log kontenera, dopóki
kontener nie zostanie usunięty, i nic na hoście go nie rotuje.
- **Postgres zapisuje, ile kosztuje każde zapytanie.** Usługa `db` ładuje
`pg_stat_statements` przy starcie, a migracja tworzy rozszerzenie. Tam, gdzie
rola bazy nie może go utworzyć, migracja loguje notice i idzie dalej.
`agenticos cmd doctor` w każdym przypadku wypisuje linię `query statistics`.
!!! note "Baza danych jest strojona wobec własnego limitu"
`docker-compose-prod.yml` uruchamia Postgresa z `shared_buffers=512MB` wobec
limitu 2 GB i daje mu 512 MB `/dev/shm` — domyślne 64 MB Dockera wyczerpuje
równoległy skan po wektorach kolekcji, raportując
`could not resize shared memory segment`. Przesuwasz limit — przesuń strojenie
razem z nim; są zapisane obok siebie właśnie dlatego.
Ustawia też `max_connections=200`, bo pule potrafią otworzyć więcej niż
domyślne 100 Postgresa: dwie pule po 15 na worker uvicorna, czyli 120 przy
domyślnych czterech, i 30 więcej dla workera zadań w tle. Podnieś je razem z
`UVICORN_WORKERS` albo `DB_POOL_SIZE`, inaczej Postgres odrzuci połączenia
z „too many clients”, zanim pula zdąży je zakolejkować albo zrzucić nadmiar.
## Skieruj nazwy na host { #point-the-names-at-the-host }
Dwa rekordy A, przed czymkolwiek innym. Let's Encrypt dowodzi, że kontrolujesz
nazwę, pobierając plik po HTTP stamtąd, gdzie ona się rozwiązuje, więc certyfikat
nie zostanie wystawiony, dopóki to nie jest prawdą i nie rozpropaguje się.
```
app.example.com A 203.0.113.10
api.example.com A 203.0.113.10
```
!!! warning "Wildcard tego za ciebie nie załatwia"
Tam, gdzie `*.example.com` już gdzieś wskazuje — zwykle na stronę marketingową
— obie nazwy rozwiązują się do niej. Rekord dla konkretnej nazwy bije
wildcard, więc naprawą jest dodanie dwóch powyżej, a nie usunięcie wildcarda.
Sprawdź ze skądś, co nie jest tym hostem, bo host może mieć własną odpowiedź:
```bash
dig +short app.example.com api.example.com
```
## Wgraj to na host { #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` zapisuje `backend/.env`: generuje pięć sekretów, pyta o dwie
nazwy hostów i o adres dla Let's Encrypt, a publiczne URL-e i origin CORS
wyprowadza z tego, co mu podałeś. O klucz providera nie pyta wcale: te mieszkają
w vaulcie każdej organizacji. Odmawia nadpisania istniejącego pliku.
Klon jest miejscem, gdzie mieszkają pliki Compose i ten plik env; żaden kod z
niego nie działa. Tym, co działa, są dwa obrazy, które publikuje repozytorium.
### Obrazy { #the-images }
| | |
|---|---|
| `ghcr.io/vstorm-co/agenticos-backend` | API, worker i migracje - jeden obraz, trzy komendy |
| `ghcr.io/vstorm-co/agenticos-frontend` | Konsola |
Oba budowane są dla `linux/amd64` i `linux/arm64` przez
`.github/workflows/images.yml`. Wydanie (`v0.0.380`) publikuje `0.0.380` i
przesuwa `latest`; każdy commit na `main` publikuje `edge` i `sha-<short>`. Pliki
Compose czytają tag z `AGENTICOS_VERSION` w `backend/.env` i domyślnie biorą
`latest`.
Trzy reguły, których trzyma się workflow, każda warta poznania przed poleganiem na
tagu:
- **Publikowany jest wyłącznie commit z `main`.** Tag `v*` wypchnięty z brancha
albo uruchomienie zlecone na branchu zostaje odrzucone, zanim cokolwiek zostanie
zbudowane - więc `latest` nie może przejść przez granicę pull requesta.
- **Wydanie na commicie, który `main` już zbudował, nie jest budowane ponownie.**
Jego manifest `sha-<short>` dostaje wersję i `latest` jako dodatkowe nazwy, więc
digesty, na których host się przypiął, są dokładnie tymi, które nazywa wydanie.
- **Commitowi bez obrazów można je dać.** Uruchom workflow ręcznie z jego wejściem
`sha` - `gh workflow run images.yml --ref main -f sha=<commit>` - a opublikuje on
tag `sha-<short>` tego commita i nic, co się przesuwa. To ścieżka dla commita
starszego niż workflow oraz dla takiego, którego uruchomienie zostało utracone.
!!! warning "Przypnij wydanie na hoście, na którym ci zależy"
`AGENTICOS_VERSION=0.0.380` w `backend/.env`, tak żeby `make prod` w zły dzień
ściągnął to, co działało wczoraj, a nie cokolwiek wydano dziś rano.
`scripts/deploy.sh` przypina za ciebie - do tagu `sha-` commita, który wdraża -
dokładnie na czas trwania wdrożenia.
Przypinaj je z checkoutu tego wydania: `git checkout v0.0.380` obok zmiennej.
Obraz usługi sandboksa jest przypięty w pliku compose, w wersji, którą mówi
backend z tego samego commita, więc starszy backend uruchomiony z nowszego
checkoutu trafia na nowszy `sandboxd` - a `latest` ze starego checkoutu to ta
sama niezgodność w drugą stronę. `scripts/deploy.sh` robi checkout commita,
który wdraża, więc skryptowe wdrożenie albo wycofanie trzyma oba razem.
Oba pakiety ściągają się anonimowo. Jeśli pobranie odpowiada `unauthorized`,
pakiet został ustawiony jako prywatny albo stare `docker login ghcr.io` stoi na
drodze; żadnego z tych host nie naprawi sam.
!!! danger "`backend/.env` trzyma klucz, który odpieczętowuje każde zapisane poświadczenie"
`VAULT_MASTER_KEY` jest tym, co czyni klucze providerów organizacji, tokeny
botów i poświadczenia MCP odczytywalnymi. Jego utrata nie zamyka ci drogi do
produktu; czyni każdy sekret w nim nieodzyskiwalnym. Zrób kopię tego pliku
gdzieś, skąd nie zabierze go zgubiony dysk, i rotuj przez
[`agenticos cmd vault-rotate`](secrets.md#operations), a nie przez edycję.
Dwie opcjonalne rzeczy, o które nie pyta, obie w tym pliku: `SMTP_*`, bez którego
nie da się wysłać zaproszeń ani resetów hasła, oraz `LOGFIRE_TOKEN`, pod który idą
ślady runów agentów.
## Wybierz reverse proxy { #choose-a-reverse-proxy }
Coś musi terminować TLS i routować te dwie nazwy. Obie opcje poniżej sięgają do
tych samych kontenerów; wybierz na podstawie tego, czy już któreś uruchamiasz.
!!! note "Kompresję zostaw aplikacji"
API kompresuje gzipem własne odpowiedzi, proxy `/api/*` konsoli ponownie
kompresuje dla przeglądarki to, co skompresowało API, a Next kompresuje swoje
strony. Proxy nie potrzebuje więc `gzip on` i nie może czyścić
`Accept-Encoding` klienta: to tylko wyłącza kompresję. Odpowiedź, która już
niesie `Content-Encoding`, Nginx i Traefik przepuszczają bez zmian.
### Opcja A: Traefik { #option-a-traefik }
Krótsza ścieżka i ta, którą wybrać na hoście, który już ma Traefika: kontenery
niosą etykiety, Traefik je odkrywa, prosi o certyfikat i go odnawia. Nic do
przeładowania i żadnego drugiego pliku konfiguracji do utrzymania w zgodzie.
Jeśli Traefika jeszcze nie ma, repozytorium dostarcza jednego:
`traefik/traefik.yml` i `docker-compose-traefik.yml`, czyli entrypoint na 443 z
resolverem Let's Encrypt i 80 przekierowującym na niego.
```bash
docker network create traefik_webgateway
docker compose --env-file backend/.env -f docker-compose-traefik.yml up -d
```
Tam, gdzie Traefik **już** działa, zostaw te pliki w spokoju i skieruj
`TRAEFIK_NETWORK` na sieć, którą on obserwuje. Nakładki czytają tę nazwę, więc nic
w istniejącym proxy nie musi się zmieniać.
Potem podnieś stack z `PROXY=traefik`, co dokłada dwa pliki nakładkowe niosące
etykiety:
```bash
make prod PROXY=traefik
make prod-frontend PROXY=traefik
```
`server-init.sh` zapisał już `PROXY=traefik` do `backend/.env`, skąd czyta to
`scripts/deploy.sh` — więc późniejsze wdrożenia trzymają się proxy, z którym ten
host został postawiony, a nie tego, które założył jakiś skrypt.
!!! info "`exposedByDefault: false` wykonuje realną robotę"
To jedno ustawienie w `traefik/traefik.yml` warte przeczytania, zanim go
uruchomisz. Tylko `app` i `frontend` niosą `traefik.enable=true`, więc
Postgres, Redis, worker i demon sandboksa są nieosiągalne z niczego
spoza hosta — a to jest właściwość *nieoznaczenia etykietą*, więc przeżywa
dodanie przez kogoś usługi bez myślenia o proxy.
!!! warning "Niektóre ścieżki żądań i query stringi są poświadczeniami"
Te segmenty ścieżki otwierają coś każdemu, kto je przeczyta:
- klucz publicznego linku, w `/a/<key>`, `/api/v1/public/artifacts/<key>` i
`/api/v1/artifact-embed/<key>`, dopóki link nie zostanie wymieniony;
- podpisany adres treści, `/api/v1/artifact-content/<token>`, przez cały
okres swojej ważności;
- token udostępnionej rozmowy, w `/shared/<token>` i
`/api/v1/conversations/shared/<token>`;
- token formularza workflow, w `/f/<token>` i
`/api/v1/public/workflow-forms/<token>`;
- token testu webhooka, w `/api/v1/workflow-webhook-tests/<token>` i
`/api/v1/workflows/<id>/webhook-test/<token>`;
- token linku kanału, w `/link/<token>` i
`/api/v1/me/channel-link/<token>`;
- token zaproszenia, w `/invitations/<token>`,
`/api/v1/invitations/<token>` i `/api/v1/invitations/<token>/accept`;
- token wznowienia przebiegu workflow, w
`/api/v1/workflow-resume/<run_id>/<token>`.
Tak samo te parametry zapytania:
- `visitor` i `token` na sockecie widgetu agenta,
`/api/v1/embed/<key>/ws`: klucz hostowanej strony, który wznawia
rozmowę odwiedzającego, oraz podpisany token odwiedzającego od klienta;
- `code` i `state` przy powrocie od dostawcy logowania,
`/api/v1/oauth/<provider>/callback`, oraz `code` na konsolowym
`/auth/callback`;
- `invitation_handle` na `/api/v1/oauth/<provider>/login` i
`/api/v1/auth/kerberos/login`, który realizuje zaproszenie, za którym
stoi;
- `token` na konsolowych `/reset-password`, `/auth/magic-link` i
`/auth/email-change` — w linkach, które wysyłają reset hasła, link
logowania i zmiana adresu e-mail.
Żądanie wysłane z jednej z tych stron może nieść jej adres w nagłówku
`Referer`. API zastępuje segmenty API i wartości tych parametrów
znacznikiem `[REDACTED]` we własnym logu dostępu, w linii uvicorna dla
każdego przyjętego WebSocketu, w każdym rekordzie logu i w spanach Logfire,
łącznie z wyjściem konsoli, argumentami endpointu i kopią, którą Logfire
robi przy starcie spanu. Pozostałe parametry zapytania, takie jak `q`,
`skip`, `limit` i identyfikatory, są zachowywane.
Traefik potrafi zachować, usunąć albo zredagować całe pole, ale nie potrafi
przepisać jego części, więc `traefik/traefik.yml` usuwa z logu dostępu
Traefika `RequestPath`, który obejmuje też query string. Nginx potrafi
przepisać część wartości, ale `nginx/nginx.conf` tak samo usuwa ścieżkę,
query string i `Referer`, formatem logu
`agenticos_no_path`, który wskazuje każdy blok `server` — dzięki temu lista
sekretnych ścieżek jest utrzymywana w jednym miejscu. Oba logi nadal zawierają
klienta, status, rozmiar i czas trwania. Log błędów Nginx zapisuje linię
żądania i `Referer` nieudanego żądania i żadne ustawienie tego nie zmienia,
więc trzymaj log błędów na hoście. Jeśli potrzebujesz ścieżek w logu dostępu,
usuń tę regułę pola albo format logu i traktuj log jak sekret: trzymaj go na
hoście i nie wysyłaj do usługi logów.
### Opcja B: Nginx { #option-b-nginx }
Dla hosta, gdzie Nginx już terminuje TLS, albo gdzie proxy w ogóle nie jest w
Dockerze. Stack publikuje oba porty na `127.0.0.1`, a Nginx sięga do nich tam:
```bash
make prod
make prod-frontend
```
`nginx/nginx.conf` jest szablonem. Dwa podstawienia, zanim cokolwiek zaserwuje:
`server_name` w każdym bloku to `${DOMAIN:-localhost}`, a Nginx tego nie rozwija —
wpisz dwie nazwy hostów ręcznie. Certyfikaty są twoje do uzyskania i odnawiania,
tak samo jak nagłówek `Strict-Transport-Security`, który backend celowo zostawia
temu, co terminuje TLS. Log dostępu w szablonie pomija ścieżkę żądania, query
string i `Referer`; ostrzeżenie *Niektóre ścieżki żądań i query stringi są
poświadczeniami* powyżej wyjaśnia dlaczego i co nadal zapisuje log błędów.
!!! warning "`BIND_HOST` jest ustawieniem bezpieczeństwa, a nie wygody"
Domyślny loopback jest tym, co nadaje limitowi prób uwierzytelnienia
jakiekolwiek znaczenie. `RATE_LIMIT_TRUST_FORWARDED_FOR` każe API liczyć próbę
na adres, który przekazuje proxy, więc cokolwiek potrafi sięgnąć do API *poza*
proxy, wybiera adres, na który liczone są jego próby. Ustaw
`BIND_HOST=0.0.0.0` wyłącznie dla proxy na innej maszynie i odfiltruj ten port
firewallem tylko do niego.
!!! warning "Frontend jest własnym projektem Compose"
Compose nazywa projekt po katalogu, więc oba stacki nazywały się
`agenticos` — a podniesienie frontendu raportowało wtedy pięć kontenerów
backendu jako **osierocone**, wraz z własną podpowiedzią Compose, żeby
uruchomić komendę ponownie z `--remove-orphans`. Posłuchanie tej rady
zatrzymuje API, bazę danych, Redisa i workera. Targety `make` i
`scripts/deploy.sh` przekazują `-p agenticos-frontend`, więc tego ostrzeżenia
już nie ma. Pliki Compose nie ustalają też na stałe nazw kontenerów - każdy
projekt nazywa własne, więc dwa stacki na jednym hoście nie mogą przejąć
swoich kontenerów nawzajem, a `deploy.sh` czeka na usługi `app` i `frontend`,
a nie na nazwę. Wdrożenie starsze niż obie te poprawki zostaje odtworzone pod
nowymi nazwami przy następnym `up`; niczego nie trzeba usuwać ręcznie.
Oba projekty wciąż spotykają się na sieci o stałej nazwie - `agenticos_edge`
na produkcji, `agenticos_backend` na serwerze deweloperskim - bo frontend
dołącza do niej jako do sieci zewnętrznej. Host uruchamiający **dwa** stacki
AgenticOS rozdziela `AGENTICOS_EDGE_NETWORK` i `AGENTICOS_DATA_NETWORK` (albo
`AGENTICOS_NETWORK`) w `backend/.env` każdego stacka; w przeciwnym razie `db`,
`redis` i `app` obu stacków rozwiązują się na jednym mostku, a żądanie może
sięgnąć do bazy danych sąsiada.
## Uruchom to i utwórz pierwsze konto { #start-it-and-create-the-first-account }
`make prod` ściąga obrazy, uruchamia stack i wykonuje migracje - te ostatnie jako
usługę `migrate`, na którą API czeka, więc `docker compose up -d` wykonane ręcznie
na tych samych plikach robi to samo. Pierwsze pobranie to około 2 GB; kolejne to
warstwy, które się zmieniły.
Następnie utwórz organizację, właściciela i działającego agenta:
```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-...
```
Klucz providera tutaj jest tym, na czym działają dwaj startowi agenci. Bez niego
zostają utworzeni i nie potrafią odpowiedzieć; każdy inny provider dodawany
jest w produkcie, per organizacja, z vaulta.
!!! tip "Sprawdź to z zewnątrz, nie z hosta"
```bash
curl -fsS https://api.example.com/api/v1/health
curl -fsSo /dev/null -w '%{http_code}\n' https://app.example.com
```
Stack, który jest zdrowy na hoście i nieosiągalny z internetu, to DNS, firewall
albo certyfikat — trzy rzeczy, których health check wewnątrz hosta nie widzi.
Potem, raz, ręcznie: zaloguj się, zaproś kogoś (co dowodzi, że działa `SMTP_*`) i
wyślij wiadomość do AgenticOS (co dowodzi klucza providera i
WebSocketa). Każda z tych czynności ćwiczy ścieżkę, której nic innego tutaj nie
sprawdza.
!!! info "Nagłówki bezpieczeństwa pochodzą z backendu, więc każde proxy jest pokryte"
Content-Security-Policy, `X-Frame-Options: DENY`,
`X-Content-Type-Options: nosniff`, `Referrer-Policy` i `Permissions-Policy`
ustawiane są na każdej odpowiedzi — łącznie z 500 dla nieobsłużonego wyjątku,
które budowane jest poza stosem middleware i stempluje je samo. Interaktywna
dokumentacja API zrzuca wyłącznie CSP, bo Swagger ładuje zasoby, których
restrykcyjna polityka zabrania.
**HSTS jest celowo pozostawione proxy**, czyli temu, gdzie terminuje TLS.
Proxy ustawiające własne CSP powinno być co najmniej tak restrykcyjne jak to.
### Usługi we własnej sieci { #services-on-your-own-network }
Żądanie skonfigurowane przez członka - krok HTTP workflow, źródło Git,
strony internetowej albo S3, agent A2A czy połączenie MCP - jest odrzucane, gdy jego host
rozwiązuje się na adres prywatny, więc wiki, serwer Git albo serwer MCP wewnątrz
Twojej sieci są domyślnie poza zasięgiem. Aby otworzyć któryś z nich, wpisz go do
`OUTBOUND_PRIVATE_ALLOWLIST` w `backend/.env`, jako host, `.suffix`, adres albo
zakres CIDR, i zrestartuj API oraz workera. W wierszu tego ustawienia w
[opisie konfiguracji](configuration.md#project-settings) są dozwolone formy, to,
co jest odrzucane niezależnie od listy, i to, do czego może wtedy sięgnąć każde
konto. Wpisuj tylko usługi, które same sprawdzają swoje poświadczenie.
`agenticos cmd doctor` pokazuje listę w wierszu `private network`. Kończy się
błędem, gdy wpisany zakres obejmuje bazę danych albo Redis, i ostrzega przed
zakresem szerszym niż /16 (IPv4) albo /48 (IPv6).
### Włączanie sandboksa { #turning-the-sandbox-on }
Usługa uruchamiająca kod agenta stoi za profilem Compose, bo to jedyny kontener
trzymający socket Dockera, a podmontowanie go na współdzielonym hoście powinno być
decyzją, a nie wartością domyślną. Trzy rzeczy, raz:
```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
```
Potem wdrożenie go podnosi: `scripts/deploy.sh` przekazuje `--profile sandbox`,
gdy `SANDBOXD_TOKEN` w `backend/.env` ma wartość, więc to sam host mówi, czy go
uruchamia. Tak jak `make` i quickstart, eksportuje `DOCKER_GID` do `group_add`
sandboksa: grupę socketu na Linuksie (po rozwiązaniu dowiązań) lub `0` na Docker
Desktop, gdzie zamontowany socket należy do maszyny wirtualnej. Jawne `DOCKER_GID`
w środowisku lub w wierszu polecenia `make` ma pierwszeństwo. Nic więcej w `.env` nie jest
potrzebne: backend sięga do demona przez *połączenie* sandboksa, które ktoś tworzy
w konsoli, a `http://sandboxd:8080` jest rozpoznawane jako własne tego
deploymentu.
!!! warning "Profil, o którym Compose nie wie, to usługa, którą Compose zatrzymuje"
`up -d` na tym samym projekcie bez `--profile sandbox` nie zostawia sandboksa
w spokoju — zatrzymuje go. Więc hostowi, który uruchomił go ręcznie, zabierało
go kolejne wdrożenie, a uruchamianie kodu agenta psuło się z powodów zupełnie
niezwiązanych z wdrożeniem, które to spowodowało (#1506). Dlatego skrypt czyta
token, zamiast przyjmować flagę.
## Wdrażanie zmiany { #deploying-a-change }
### Pamięć natywna i ciągłość rutyn { #native-memory-upgrade }
Przed aktualizacją wykonaj [kopie bazy, mediów i kluczy sejfu](#backups)
i skoordynuj wdrożenie API oraz workerów. Użyj zwykłego kroku migracji wdrożenia;
nie kopiuj ręcznie wierszy ani nie twórz zastępczych harmonogramów.
Przed zastosowaniem `0285_member_proactivity.py` zatrzymaj API i procesy zlecające sweepy, poczekaj na zakończenie aktywnych zadań proaktywnych, a następnie zatrzymaj workery. Migracja odmawia działania, gdy trwa zadanie modelu obserwacji lub nocnego przeglądu, i uzupełnia kolejkę o wspólny klucz współbieżności na osobę. Aktualizuj API i workery razem, aby nowe zadania używały tego samego klucza; obserwacja i nocny przegląd jednej osoby nie mogą niezależnie wydać tego samego pozostałego limitu.
- `0248_drop_memory_mem0.py` zatrzymuje aktualizację, jeśli `memory_mem0` pozostaje
w którymkolwiek zapisanym szkicu lub opublikowanej specyfikacji. Przed ponowieniem
zinwentaryzuj wszystkie endpointy i powiązania poświadczeń sejfu z kopii zapasowej,
usuń zewnętrzne wspomnienia i potwierdź zakończenie u każdego dostawcy. Dopiero
potem jawnie usuń te bindingi z `agents.draft_spec` i `agent_versions.spec`.
Sam eksport nie oznacza usunięcia. Zachowaj dowody czyszczenia, przejrzyj nieużywane
klucze sejfu i unieważnij wycofane poświadczenia. Migracja nie wykonuje zdalnych
żądań ani nie zakłada udanego usunięcia. Jeśli wcześniejsza wersja rozwojowa już
usunęła bindingi, odtwórz inwentarz z kopii sprzed aktualizacji i wykonaj to samo
czyszczenie; natywne usuwanie pamięci osoby lub agenta nie dociera do tych danych.
- `0251_primary_conversations.py` i `0258_primary_channel_history.py` ustanawiają
główne czaty prywatne. Istniejące przypisanie ma pierwszeństwo. Tylko dowodnie
prywatna historia powiązanego kanału jest przejmowana jednorazowo; historie
projektowe, grupowe i niejednoznaczne pozostają oddzielne. Nie uruchamiaj
ponownie uzupełnienia po wyczyszczeniu głównego czatu przez osobę.
- Istniejące identyfikatory rutyn, harmonogramy i logi pozostają na miejscu.
`0255_trigger_delivery.py` nadaje starym rutynom `run_log`; nowe domyślnie
wybierają główny czat albo zaufaną rozmowę źródłową. W **Rutynach** sprawdź
odbiorcę wyniku, działającą osobę, koniec i budżet, zamiast tworzyć kopie.
- `0259_trigger_version_pin.py` dodaje przypięcie niezmiennej wersji. Pin i nazwane
środowisko wzajemnie się wykluczają. Usuń pin przed usunięciem wersji.
Zaparkowany run zachowuje przyjętą wersję; `0260_routine_run_origin.py` zachowuje
jego pochodzenie, więc usunięcie rutyny nie zwalnia kontroli uprawnień.
- `0261_watch_attempt_status.py` zaczyna jawne śledzenie prób bez zgadywania starszej
historii sukcesów i błędów. Niedostępne lub nieukończone sprawdzenie nie przesuwa
daty sukcesu. Przejrzyj istniejące obserwacje i ich dostęp do kont: historyczne
wiersze nie dowodzą, w jaki sposób pierwotnie udzielono zgody.
Przed włączeniem utrzymania pamięci wybierz model jawnie; poranny briefing
zachowuje dotychczasowe ustawienia modelu i budżetu organizacji. Od `0262_watch_agent_sources.py` obserwacje wykonuje wybrany agent, a runy
zachowują zatwierdzone źródła. Istniejące obserwacje wymagają wybrania agenta
i zatwierdzenia źródeł; identyfikatory i ustalenia pozostają. Sprawdź [zarządzanie](governance.md#proactive-work)
i [konfigurację kanałów](channels.md#sms), w tym `PUBLIC_BASE_URL` i wygasłe
powiązania SMS. W organizacji testowej sprawdź prywatny czat, Aktywność i dostawę
jednej rutyny, pauzę/wygaśnięcie oraz obserwację niedostępnego źródła, zanim
rozszerzysz wdrożenie.
### Granica formatu historii w 1.25.0 { #retry-history-upgrade }
Wersja 1.25.0 przechodzi na wyniki narzędzi `retried` i osobne wiadomości
`retry-feedback` z Pydantic AI 2.61. Istniejące historie i checkpointy przebiegów
są odczytywane bez migracji bazy ani ponownej publikacji agentów. Historie zawierające te części
wymagają nowego czytnika: starszy backend nie odtworzy tych formatów wiadomości.
Aktualizuj wszystkie procesy API i workery razem, kończąc pracę lub zatrzymując
stare procesy, zanim nowe zaczną przyjmować zadania. Nie łącz starszych i nowszych
replik backendu z tą samą bazą. Po zapisaniu nowego formatu retry wycofanie samego kodu
przez tę granicę nie jest obsługiwane; trzeba zachować nowy czytnik albo odtworzyć
snapshot bazy sprzed aktualizacji.
### Ręcznie { #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` pobiera ten commit, czeka na obrazy, które CI dla niego
opublikowało (`sha-<short>`, zwykle już tam są), ściąga je, restartuje i czeka, aż
oba kontenery zgłoszą się jako zdrowe, zanim zwróci kod niezerowy albo nie.
Przyjmuje **commit**, a nie branch, więc tym, co jest wdrożone, jest to, co zostało
zrecenzowane, a nie to, dokąd `main` przesunął się od tamtej pory - a tym, co
działa, jest bajt w bajt to, co zbudowało CI, na hoście, który nigdy nie potrzebuje
łańcucha narzędzi.
!!! warning "Skopiuj go na host, a potem uruchom — nie wpychaj go do `bash -s`"
Pod `bash -s` skrypt jest własnym standardowym wejściem powłoki, a pierwsza
komenda w nim, która czyta stdin, pochłania resztę. `docker compose exec`
przekazuje stdin do kontenera nawet z `-T`, więc migracja zjadła wszystko
poniżej siebie, bash doszedł do EOF, a wdrożenie zakończyło się **0**, nigdy
nie zbudowawszy frontendu ani nie czekając na żaden kontener. Witryna leżała, a
wdrożenie było zielone ([#1488](https://github.com/vstorm-co/agenticos/issues/1488)).
Dwa połączenia zamiast jednego są tym, co odbiera procedurze możliwość obcięcia
samej siebie.
To nie jest wdrożenie bez przestoju. Compose odtwarza kontenery, których obraz się
zmienił, więc witryna jest niedostępna przez te kilka sekund, które to zajmuje.
### Retencja obrazów wydań { #release-image-retention }
Przed pobraniem skrypt usuwa stare, nieużywane obrazy wydań wyłącznie z
`ghcr.io/vstorm-co/agenticos-backend`, `agenticos-frontend` i `agenticos-browser`.
Zachowuje dwa dodatkowe nieużywane identyfikatory obrazów na repozytorium, od
najnowszych. Wiele tagów SHA lub wersji jednego ID liczy się jako jeden obraz.
Usuwane mogą być wyłącznie tagi `sha-<hex>` i numery wersji. Wszystkie aliasy
usuwanego ID znikają razem, bez `--force`. Flaga `--no-prune` zapobiega też
niejawnemu usuwaniu obrazów nadrzędnych bez tagów.
Każdy działający lub zatrzymany kontener na hoście chroni swój obraz, także w
innym projekcie Compose. Obraz docelowy, dowolny tag zmienny lub ręczny oraz tag
lub digest w innym repozytorium również chronią całe ID. Obrazy sandboxów i
środowisk wykonawczych, obrazy bez tagów oraz wolumeny aplikacji pozostają poza
sprzątaniem. Te zabezpieczenia mogą zachować więcej obrazów niż limit obrazów
nieużywanych.
Skrót katalogu wdrożenia rozdziela przypięcia stosów na wspólnym hoście.
Lokalne tagi `deploy-<stack-hash>-current` i `deploy-<stack-hash>-rollback` zachowują ostatni zdrowy obraz
każdego komponentu i jego poprzednika po wymianie kontenerów. Przy pierwszym
uruchomieniu `deploy-<stack-hash>-current` powstaje ze zdrowych kontenerów tego stosu, nie z
checkoutu; różne zdrowe ID tego samego komponentu zatrzymują wdrożenie. Przypięcia
zmieniają się dopiero po pomyślnych testach zdrowia nowych usług. Nieudane
wdrożenia zachowują zapisane przypięcia, a ponowne wdrożenie tego samego obrazu
nie zastępuje jego rollbacku. Cofanie nadal wymaga zatwierdzonego SHA commita;
te lokalne tagi są znacznikami retencji, nie wydaniami w rejestrze.
Skrypt wymaga co najmniej **5 GiB wolnego miejsca** na systemie plików magazynu
Dockera przed każdym pobraniem i po opcjonalnym przygotowaniu środowisk
wykonawczych. Sprawdza `DockerRootDir` oraz domyślny magazyn
`/var/lib/containerd`, jeżeli istnieje. To minimalny próg pojemności, nie
rezerwacja ani oszacowanie wielkości obrazów; niestandardowe magazyny containerd
monitoruj osobno. Obrazy backendu i frontendu są pobierane przed restartem
którejkolwiek usługi. Odmowa z powodu braku miejsca podaje system plików i
dostępną pojemność: zwolnij miejsce poza chronionymi zasobami lub powiększ
magazyn, a następnie ponów zatwierdzone wdrożenie. Nie realizuj tej polityki
przez globalne `docker image prune -a`.
W Docker Desktop katalog magazynu znajduje się wewnątrz maszyny wirtualnej
Linux. Skrypt mierzy wolne miejsce w VM za pomocą tymczasowego kontenera tylko
do odczytu, bez sieci, z już lokalnie dostępnym obrazem backendu, najlepiej
ostatnim zdrowym. Kontener pomiarowy nie może pobierać obrazów i jest usuwany
automatycznie. Skrypt sprawdza też system plików hosta z katalogiem wdrożenia;
system plików niestandardowego dysku VM monitoruj osobno. Ta procedura
aktualizacji wymaga lokalnego obrazu backendu; najpierw zakończ zwykłą pierwszą
instalację.
### Z GitHuba, z zatwierdzeniem { #from-github-with-an-approval }
`.github/workflows/deploy.yml` oferuje każde opublikowane stabilne wydanie do wdrożenia i czeka,
aż ktoś to zatwierdzi. Ta bramka jest **ustawieniem repozytorium, a nie krokiem w
pliku** — bez niej workflow wdraża każde opublikowane stabilne wydanie bez nadzoru.
Skonfiguruj to raz:
1. **Settings → Environments → New environment**, nazwane `production`.
2. Zaznacz **Required reviewers** i dodaj tych, którzy mogą zatwierdzać. To jest ta
bramka.
3. Dodaj zmienne tego środowiska: `SITE_URL`, `API_URL` oraz `APP_DIR`, jeśli
checkout nie jest w `/opt/agenticos`.
4. Dodaj sekrety wymienione niżej.
!!! warning "Anuluj wdrożenie, którego nie zamierzasz zatwierdzić"
Każde uruchomienie dzieli grupę współbieżności `deploy-production`, a
uruchomienie siedzące przy bramce zatwierdzenia ją trzyma. Nie wygasa samo z
siebie — GitHub anuluje takie, na które nie zareagowano, po 30 dniach — więc
dopóki ktoś go nie zatwierdzi albo nie anuluje, późniejsze wydania'e stoją w
kolejce za decyzją, której nikt nie podejmie, a serwer dalej uruchamia to, co
zostało wdrożone ostatnio.
Więc wdrożenie, przeciw któremu się zdecydowałeś, anulujesz, a nie zostawiasz.
Jedno zostawione w oczekiwaniu na nieaktualny commit zablokowało tutaj trzy
późniejsze uruchomienia, zanim ktokolwiek zauważył kolejkę, a nie uruchomienia.
`deploy-queue.yml` jest tym, co dzieje się, kiedy mimo wszystko zostanie
zostawione: co sześć godzin anuluje uruchomienie Deploy czekające na
zatwierdzenie dłużej niż dwanaście i zakłada zgłoszenie z nazwą tego
uruchomienia oraz z tym, ile commitów dzieli najnowsze stabilne wydanie od ostatniego udanego
wdrożenia. Niczego nie zatwierdza i niczego nie wdraża — opróżnia kolejkę, żeby
następne opublikowane wydanie dotarło do bramki, i mówi, że to zrobił. Ręcznie uruchamiasz je
przez `gh workflow run deploy-queue.yml`.
!!! danger "Zatwierdzanie z kolejki środowiska zatwierdza **najstarsze** uruchomienie"
Czyli to nieaktualne. Późne zatwierdzenie wydane tam wdraża commit z chwili,
w której kolejka się zakleszczyła, a nie ten dopiero co wypchnięty — tak właśnie
na ten serwer trafił commit sprzed dwóch tygodni. Zatwierdzaj ze strony
uruchomienia dla commita, o który ci chodzi, albo zacznij nowe przez
`gh workflow run deploy.yml --ref main -f ref=v1.34.3` i zatwierdź je.
| Sekret | Co |
|---|---|
| `DEPLOY_HOST` | Adres hosta |
| `DEPLOY_USER` | Konto, do którego należy checkout |
| `DEPLOY_SSH_KEY` | Klucz prywatny, którego połowa publiczna jest w `authorized_keys` tego konta |
| `DEPLOY_KNOWN_HOSTS` | `ssh-keyscan your-host`, uruchomione skądś, komu ufasz |
Wygeneruj klucz do tego i do niczego więcej:
```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 "Klucz hosta jest sekretem, a nie `ssh-keyscan` w czasie wdrożenia"
Skanowanie w czasie wdrożenia ufa temu, co odpowiada pod tym adresem, czyli
dokładnie temu, czemu klucz hosta ma zapobiegać. Zeskanuj raz, skądś, komu
ufasz, i zapisz odpowiedź.
Pojawia się wtedy uruchomienie z **Review deployments**; zatwierdzenie go
uruchamia job. `workflow_dispatch` uruchamia ten sam job wobec wskazanego przez
ciebie refa, i tak właśnie robi się rollback, a przechodzi on przez to samo
zatwierdzenie.
## Kopie zapasowe { #backups }
Liczy się jeden wolumen i nie jest oczywiste który:
| Wolumen | Trzyma | Kopia |
|---|---|---|
| `postgres_data` | wszystko — agentów, konwersacje, zapieczętowane poświadczenia | **tak** |
| `media_data` | wgrane pliki, przed ingestią | tak |
Cache nie ma już żadnego wolumenu. Kubełki limitów, znaczniki deduplikacji
kanałów i odpowiedzi o członkostwie mają TTL i odtwarzają się same, więc Valkey
działa z `--save ''` i po restarcie startuje pusty. Host, na którym działało
wcześniejsze wydanie, nadal trzyma wolumen `agenticos_redis_data`, którego nic
już nie montuje; usuwa go `docker volume rm agenticos_redis_data`.
```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"
```
Identyfikatory pochodzą z własnego środowiska kontenera, zamiast być wypisane,
ponieważ oba są ustawieniami: wdrożenie, które zmieniło którekolwiek z nich,
dostałoby w przeciwnym razie pusty plik i błąd, którego nikt nie przeczyta po
drodze.
!!! danger "Kopia bazy danych bez `backend/.env` nie jest kopią zapasową"
Poświadczenia w niej są zapieczętowane `VAULT_MASTER_KEY`. Odtworzone obok
innego klucza, każdy klucz providera, token bota i poświadczenie MCP w zrzucie
są nie do odczytania — a produkt powie ci to, jedną odmową na raz.
## Wycofywanie zmian { #rolling-back }
| | Jak |
|---|---|
| **Kod** | Wdróż poprzedni commit: `workflow_dispatch` z jego sha albo `scripts/deploy.sh`. Obrazy wciąż są w rejestrze, więc to pobranie, a nie budowanie. Commit bez obrazów `sha-<short>` - starszy niż `images.yml` albo taki, którego uruchomienie zaginęło - publikuje się najpierw przez `gh workflow run images.yml --ref main -f sha=<commit>`; wdrożenie nazywa tę komendę, kiedy przestaje czekać |
| **Schemat** | `agenticos db downgrade --revision=-1`, a potem wdróż kod, który do niego pasuje |
| **Dane** | `pg_restore` ze zrzutu, a potem sprawdź migrację, której oczekuje kod |
Wycofanie kodu **przez migrację jest decyzją, a nie komendą**. Stary kod spotyka
schemat, którego nigdy nie widział; czy to zadziała, zależy od migracji. Przeczytaj
ją, zanim cokolwiek założysz.
## Podsumowanie { #recap }
- **Jeden host, Compose, proxy z przodu.** Siedem kontenerów - jeden z nich
wykonuje migracje i kończy działanie - dwa z nich ze stanem, każdy ściągnięty -
nic nie jest budowane na hoście. Przypnij `AGENTICOS_VERSION`.
- **`UVICORN_WORKERS` decyduje, ile kosztuje host.** 460 MiB na workera, nic
współdzielonego. Dwóch dla zespołu, czterech dla prawdziwego ruchu.
- **DNS przed wszystkim.** Żaden certyfikat nie zostaje wystawiony, dopóki nazwy
nie rozwiązują się do hosta.
- **Zatwierdzenie jest ustawieniem repozytorium**, a nie linią w workflow. Bez
wymaganych recenzentów na środowisku `production` każdy merge wdraża się sam.
- **Rób kopię `postgres_data` i `backend/.env` razem.** Jedno bez drugiego nie jest
odtworzeniem.