Docker Compose
The repo's root docker-compose.yml is the single entry point for self-hosting Clone. Four services, one Postgres volume, and a handful of environment variables.
Bring it up
cp .env.example .env
# Edit .env: POSTGRES_PASSWORD, ANTHROPIC_API_KEY, ELEVENLABS_API_KEY, etc.
docker compose -f docker-compose.local.yml up --build
docker-compose.local.yml builds every service from source on each up and uses HTTP / non-privileged ports (http://localhost:8080). See the repo root README for details.
Postgres data persists in the named volume pgdata and survives container recreates.
Service reference
db: postgres:16-alpine # internal port 5432
api: build apps/server/Dockerfile # internal port 8000
mcp: build apps/mcp/Dockerfile # internal port 3000
web: build apps/web/Dockerfile # exposed 80, 443
db
Postgres 16. Healthchecked with pg_isready -U clone. Uses POSTGRES_PASSWORD from .env. Database name clone, user clone.
api
Builds from the repo root so packages/schema/events.schema.json is reachable by recording.views (which loads it via parents[3]).
Key env vars:
| Var | Purpose |
|---|---|
DATABASE_URL | postgresql://clone:${POSTGRES_PASSWORD}@db:5432/clone |
DEBUG | "False" in production. |
ALLOWED_HOSTS | clone.is,www.clone.is,api.clone.is,api,localhost — api and localhost are required for in-container service-to-service calls (e.g. mcp → http://api:8000) since Django's DisallowedHost check matches the Host header verbatim. |
ANTHROPIC_API_KEY | Required for Prediction and memory promotion. |
mcp
environment:
MCP_TRANSPORT: http
PORT: 3000
CLONE_API_URL: http://api:8000
# CLONE_API_TOKEN intentionally unset — HTTP mode reads the bearer
# off each request so one MCP instance serves many users.
depends_on:
- api
web
ports:
- "80:80"
- "443:443"
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- api
- mcp
The build context is the repo root because apps/web/public/* symlinks into packages/design/assets/, and the Vite alias @clone/design resolves to ../../packages/design. The same image bundles the docs build (apps/docs/build/ → /usr/share/nginx/html/docs/) so /docs/ works without a separate hosting target.
Migrations and admin
# One-shot Django commands run in the api service.
docker compose run --rm api python manage.py migrate
docker compose run --rm api python manage.py createsuperuser
Persistence
pgdatanamed volume — Postgres data. Back this up; nothing else in the stack is stateful on disk./etc/letsencrypt(host) — TLS certs, mounted read-only into the web container.
Health and logs
docker compose ps # which services are up
docker compose logs -f api # follow Django logs
docker compose logs -f web # follow nginx + access logs
docker compose exec db psql -U clone # open a psql session
When something breaks
- API responds with
400 DisallowedHost—ALLOWED_HOSTSdoesn't include theHostheader value the request arrived with. Add the host (often the internal one used by service-to-service calls). - MCP returns 502 from
web— themcpservice is not listening on 3000 yet (still booting), ornginx.conf's/mcpblock lost itsproxy_buffering offsetting (Streamable HTTP requires it). - Predictions return 503 —
ANTHROPIC_API_KEYis missing, invalid, or rate-limited (the server collapses these into 503 / 429 — seeapps/server/predictions/views.py).
DNS-specific gotchas — including the api.clone.is NXDOMAIN issue that bit the live deploy — live in DNS & TLS.