Deploy with Docker Compose
This page covers what every deployment has in common (the process model, the images and the secrets) and then the single-host deployment in deploy/compose/. For Kubernetes, read this page’s first three sections and continue with Deploy with Helm.
Process model
Section titled “Process model”| Process | Image and command | Needs | Talks to |
|---|---|---|---|
| API | polo-api api |
application database role, Redis, Temporal, S3 storage, worker keys, encryption keys | browsers and workers (inbound) |
| Outbox relay | polo-api relay |
dispatch database role, Temporal | PostgreSQL, Temporal |
| Scheduler | polo-api scheduler |
its worker token, the API origin | the API |
| Runtime worker | polo-runtime |
its worker token, the API origin, Temporal, the model registry and model credentials, optionally a Docker socket | the API, Temporal, model providers, Docker |
| Migrate and provision | polo-api migrate, polo-api provision |
owner database role, role passwords | PostgreSQL |
| Watch processes (optional) | polo-api calendar-watch, gmail-intake-watch, managed-gmail-watch, source-followthrough-watch, knowledge-indexer |
one dedicated worker token each | the API |
| Customer runners | polo-tools with RUNNER=terraform and so on |
encrypted runner configuration, storage | the systems they operate |
Only the API holds database credentials for product state. Workers authenticate to it with dedicated tokens listed in POLO_API_WORKER_KEYS. The scheduler, the worker and the watch processes accept an API origin only when it is HTTPS or loopback, so a deployment either terminates TLS in front of the API or lets those processes share the API’s network namespace. Dependencies are PostgreSQL with pgvector, Redis, Temporal and S3-compatible object storage.
For how these pieces fit together, see Architecture.
Images
Section titled “Images”All Dockerfiles build from the repository root, pin their base images by digest and run as a non-root user.
docker build -f api/Dockerfile -t polo-api:<tag> .docker build -f runtime.Dockerfile -t polo-runtime:<tag> .docker build -f tools.Dockerfile -t polo-tools:<tag> .docker build -f engineering.Dockerfile -t polo-engineering-tools:<tag> .polo-apiis the control plane. Its entrypoint takes the process name as the first argument:api(default),relay,scheduler,migrate,provision,calendar-watch,gmail-intake-watch,managed-gmail-watch,source-followthrough-watchorknowledge-indexer. It listens on port 8088 and runs as thenodeuser.polo-runtimeis the agent worker (python -m runtime). It includesgitand the Docker CLI and runs as uid 65532. At start it copies the read-only model registry named byPOLO_RUNTIME_MODELS_SOURCEto a private0600file.polo-toolsruns customer runners, selected byRUNNER. It ships Terraform, kubectl, Helm, the PostgreSQL 17 clients, git, the Docker CLI and bubblewrap, runs as uid 65532 and listens onRUNNER_PORT(default 8090). See Runners.polo-engineering-toolsis the sandbox toolchain. The worker starts it through the Docker daemon it is given, so the image must be present on that daemon (the Compose host, or each worker node). It is not pulled from a registry by default.
Secrets
Section titled “Secrets”Generate every value once per environment and keep it in your secret manager. Values are embedded in connection strings, so keep them URL-safe.
| Secret | Used by | Generate |
|---|---|---|
polo_migration password |
migrate and provision; Temporal persistence in Compose | openssl rand -hex 24 |
polo_application password |
API, provision | openssl rand -hex 24 |
polo_dispatch password |
relay, provision | openssl rand -hex 24 |
Runtime worker token (POLO_RUNTIME_WORKER_TOKEN) |
worker | python3 -c 'import secrets; print(secrets.token_urlsafe(36))' |
Scheduler token (POLO_SCHEDULER_WORKER_TOKEN) |
scheduler | same |
| Watch-process tokens | one per enabled watcher | same |
POLO_API_WORKER_KEYS |
API: JSON object of worker id to token, at least 32 characters per token | composed from the tokens above |
POLO_API_OPERATION_KEY |
API: encrypts stored connection and model credentials | openssl rand -base64 32 |
POLO_API_SSO_KEY |
API: encrypts SSO state | openssl rand -base64 32 |
POLO_API_ARTIFACT_ACCESS_KEY, POLO_API_ARTIFACT_SECRET_KEY |
API: S3 credentials scoped to the artifact bucket | from the storage provider |
| Model credentials | worker only; one variable per api_key_env in the model registry |
from the provider |
POLO_API_METRICS_TOKEN (optional) |
API and scheduler: bearer token for GET /metrics |
openssl rand -hex 32 |
To rotate a worker token, update POLO_API_WORKER_KEYS, restart the API, then restart the worker with the new token.
Deploy the Compose stack
Section titled “Deploy the Compose stack”deploy/compose/docker-compose.yml runs PostgreSQL with pgvector, Redis, Temporal (persisting to the same PostgreSQL server), the one-shot migrate and provision step, the API, the relay, the scheduler and the runtime worker on one host.
Prerequisites
Section titled “Prerequisites”- Docker Engine with Compose v2
- A DNS name with a TLS certificate for the public origin
- A TLS-terminating reverse proxy on the host (Caddy, nginx or Traefik)
- An S3-compatible bucket reachable over HTTPS: AWS S3, or the MinIO overlay described below
-
Build or pull the three images and the toolchain image (
polo-engineering-tools:devby default). -
Create the environment file and fill in every empty value. The file documents each variable.
Terminal window cd deploy/composecp .env.example .envchmod 600 .env -
Create the worker’s workspace root and the model registry on the host. Both belong to the worker’s uid 65532, and the registry is a private file.
Terminal window sudo install -d -m 0700 -o 65532 -g 65532 /var/lib/polo/workspacessudo install -d -m 0755 /etc/polosudo install -m 0600 -o 65532 -g 65532 /dev/null /etc/polo/models.jsonecho '{}' | sudo tee /etc/polo/models.json > /dev/nullFill
/etc/polo/models.jsonwith the model routes your workspaces may use, and list the same routes for the API inPOLO_HOSTED_MODEL_ROUTES. The format is shown in.env.example. The file must exist before the firstup; the stack refuses to start without it. -
Validate, then start.
Terminal window docker compose --env-file .env -f docker-compose.yml config -qdocker compose --env-file .env -f docker-compose.yml up -ddocker compose --env-file .env -f docker-compose.yml logs -f migrate api workerThe
migrateservice runspolo-api migrate && polo-api provisionand exits. The API starts only after it succeeded, and the scheduler and worker start once the API is healthy. -
Check readiness.
Terminal window curl -fsS http://127.0.0.1:8088/readyzIt answers
{"status":"ready",...}once PostgreSQL, Redis and Temporal answer./healthzis liveness only. -
Point the reverse proxy at
127.0.0.1:8088for the host named inPOLO_PUBLIC_URL, and have it setX-Forwarded-For. The API rejects plain HTTP for a non-loopback origin.
Client addresses and the reverse proxy
Section titled “Client addresses and the reverse proxy”Sign-in, signup, SSO, invitation and password-reset rate limits count per client address. The host’s reverse proxy reaches the API from the gateway of the stack’s Docker network, so the stack pins that network (POLO_NETWORK_SUBNET, default 172.30.88.0/24, and POLO_NETWORK_GATEWAY, default 172.30.88.1) and the API trusts X-Forwarded-For from that gateway only.
- Caddy and Traefik set
X-Forwarded-Forby default. nginx needsproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. - If the subnet collides with another network on the host, choose a free one and set both variables.
- If the proxy reaches the API another way (from another host through
POLO_BIND_ADDRESS, or as a container on this network), setPOLO_TRUSTED_PROXIESto exactly the addresses it connects from, and keep the API port unreachable for everyone else. A trusted address can claim any client address.
Without a trusted proxy, every user shares one rate-limit bucket.
Object storage
Section titled “Object storage”Object storage must be HTTPS.
-
AWS S3: set
POLO_ARTIFACT_ENDPOINT=https://s3.<region>.amazonaws.comand use a key scoped to the bucket. -
Self-hosted MinIO: add the overlay and set
POLO_ARTIFACT_ENDPOINT=https://minio:9000,POLO_MINIO_IMAGEandPOLO_TLS_DIR. The TLS directory holdsminio/public.crt(with SANminio),minio/private.keyandca.crt. MinIO no longer publishes public images, so build or mirror your own. The overlay creates the bucket and mounts the CA into the API.Terminal window docker compose --env-file .env -f docker-compose.yml -f minio.yml up -d
The worker and the Docker socket
Section titled “The worker and the Docker socket”The worker mounts the Docker socket to run repository checks in sandbox containers.
Optional processes
Section titled “Optional processes”-
Temporal UI:
docker compose --profile ui up -dadds it onPOLO_TEMPORAL_UI_PORT(loopback, default 8238). -
Metrics: set
POLO_METRICS_TOKENto enableGET /metricson the API and on the scheduler’s listener (POLO_SCHEDULER_METRICS_PORT, default 9464). See Operate. -
Watch processes use the same image and pattern as the scheduler. Add a service like this, and register its token in the API’s
POLO_API_WORKER_KEYStogether withPOLO_API_CALENDAR_WORKER_ID=calendar:calendar-watch:image: ${POLO_API_IMAGE:-polo-api:latest}command: [calendar-watch]network_mode: service:apienvironment:POLO_CALENDAR_API_URL: http://127.0.0.1:8088POLO_CALENDAR_WORKER_ID: calendarPOLO_CALENDAR_WORKER_TOKEN: ${POLO_CALENDAR_TOKEN:?}
The scheduler and the worker use network_mode: service:api and reach the API at http://127.0.0.1:8088 inside the API container’s namespace. If you split them across hosts, replace that address with the public HTTPS URL.
Upgrade
Section titled “Upgrade”- Build and publish the new images. Read the release notes for new environment variables and add them to
.envfirst. - Back up PostgreSQL; see Backup and restore.
- Run
docker compose --env-file .env -f docker-compose.yml up -d. Themigrateservice runs before the API on everyup.
The worker drains in-flight activities during its 120-second stop grace period; new work waits on the Temporal queue until a worker polls again. The full order and what to do when a step fails are in Deploy with Helm.