Skip to content

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 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.

All Dockerfiles build from the repository root, pin their base images by digest and run as a non-root user.

Terminal window
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-api is 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-watch or knowledge-indexer. It listens on port 8088 and runs as the node user.
  • polo-runtime is the agent worker (python -m runtime). It includes git and the Docker CLI and runs as uid 65532. At start it copies the read-only model registry named by POLO_RUNTIME_MODELS_SOURCE to a private 0600 file.
  • polo-tools runs customer runners, selected by RUNNER. It ships Terraform, kubectl, Helm, the PostgreSQL 17 clients, git, the Docker CLI and bubblewrap, runs as uid 65532 and listens on RUNNER_PORT (default 8090). See Runners.
  • polo-engineering-tools is 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.

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/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.

  • 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
  1. Build or pull the three images and the toolchain image (polo-engineering-tools:dev by default).

  2. Create the environment file and fill in every empty value. The file documents each variable.

    Terminal window
    cd deploy/compose
    cp .env.example .env
    chmod 600 .env
  3. 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/workspaces
    sudo install -d -m 0755 /etc/polo
    sudo install -m 0600 -o 65532 -g 65532 /dev/null /etc/polo/models.json
    echo '{}' | sudo tee /etc/polo/models.json > /dev/null

    Fill /etc/polo/models.json with the model routes your workspaces may use, and list the same routes for the API in POLO_HOSTED_MODEL_ROUTES. The format is shown in .env.example. The file must exist before the first up; the stack refuses to start without it.

  4. Validate, then start.

    Terminal window
    docker compose --env-file .env -f docker-compose.yml config -q
    docker compose --env-file .env -f docker-compose.yml up -d
    docker compose --env-file .env -f docker-compose.yml logs -f migrate api worker

    The migrate service runs polo-api migrate && polo-api provision and exits. The API starts only after it succeeded, and the scheduler and worker start once the API is healthy.

  5. Check readiness.

    Terminal window
    curl -fsS http://127.0.0.1:8088/readyz

    It answers {"status":"ready",...} once PostgreSQL, Redis and Temporal answer. /healthz is liveness only.

  6. Point the reverse proxy at 127.0.0.1:8088 for the host named in POLO_PUBLIC_URL, and have it set X-Forwarded-For. The API rejects plain HTTP for a non-loopback origin.

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-For by default. nginx needs proxy_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), set POLO_TRUSTED_PROXIES to 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 must be HTTPS.

  • AWS S3: set POLO_ARTIFACT_ENDPOINT=https://s3.<region>.amazonaws.com and use a key scoped to the bucket.

  • Self-hosted MinIO: add the overlay and set POLO_ARTIFACT_ENDPOINT=https://minio:9000, POLO_MINIO_IMAGE and POLO_TLS_DIR. The TLS directory holds minio/public.crt (with SAN minio), minio/private.key and ca.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 mounts the Docker socket to run repository checks in sandbox containers.

  • Temporal UI: docker compose --profile ui up -d adds it on POLO_TEMPORAL_UI_PORT (loopback, default 8238).

  • Metrics: set POLO_METRICS_TOKEN to enable GET /metrics on 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_KEYS together with POLO_API_CALENDAR_WORKER_ID=calendar:

    calendar-watch:
    image: ${POLO_API_IMAGE:-polo-api:latest}
    command: [calendar-watch]
    network_mode: service:api
    environment:
    POLO_CALENDAR_API_URL: http://127.0.0.1:8088
    POLO_CALENDAR_WORKER_ID: calendar
    POLO_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.

  1. Build and publish the new images. Read the release notes for new environment variables and add them to .env first.
  2. Back up PostgreSQL; see Backup and restore.
  3. Run docker compose --env-file .env -f docker-compose.yml up -d. The migrate service runs before the API on every up.

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.