Deployment architecture
Understand Clouisle service topology, request routing, dependencies, and persisted data
Clouisle runs 5 application services using 3 images. clouisle-backend provides api, worker, and beat; clouisle-sandbox-worker provides sandbox-worker; clouisle-frontend provides the Next.js frontend.
Service topology
| Service | Image | Role | Port |
|---|---|---|---|
frontend | clouisle-frontend | Next.js standalone server | 3000 |
api | clouisle-backend | FastAPI API | 8000 |
worker | clouisle-backend | Celery default, agent, knowledge, workflow queues | None |
sandbox-worker | clouisle-sandbox-worker | Celery sandbox queue and artifact uploads | None |
beat | clouisle-backend | Celery Beat scheduler, must run with a single replica | None |
db | PostgreSQL 17 + pg_search | Business, full-text retrieval, and audit data | 5432 |
redis | Redis 7 | Queues, cache, sessions | 6379 |
qdrant | Qdrant 1.18.3 | Vector index | 6333 |
Celery queue layout
The clouisle-backend image starts api, worker, and beat through subcommands. Task-to-queue routing is declared statically in task_routes in backend/app/core/celery.py, and the consumed queue names are passed with -Q.
| Queue | Consumer | Task modules | Work it carries |
|---|---|---|---|
default | worker | app.tasks.usage, notification, audit_log, api_key, password_expiration, memory | Usage resets, notification delivery, audit log archiving, API key and password expiry checks |
agent | worker | app.tasks.agent | AgentRun execution (model calls and tool loop) |
knowledge | worker | app.tasks.knowledge_base | Document parsing, chunking, embedding, and indexing |
workflow | worker | app.tasks.workflow | Workflow runs |
sandbox | sandbox-worker | app.tasks.sandbox, tasks.cleanup_expired_sandbox_sessions | Code sandbox execution and expired sandbox session cleanup |
The supplied Compose, Helm, and single-file manifests run the worker as python main.py worker -c 4 -Q default,agent,knowledge,workflow — one service consuming all four queues — and the sandbox worker as python main.py sandbox-worker -c <SANDBOX_WORKER_CONCURRENCY>, consuming only sandbox.
Periodic tasks dispatched by Beat: sweep-lost-agent-runs every 2 minutes on the default queue (reclaims AgentRuns still marked in-flight after a worker crash and is the only producer of INTERRUPTED), and cleanup-expired-sandbox-sessions every 15 minutes on the sandbox queue. Tasks on the agent queue set no retries: model calls and tool execution have side effects, so a retry would execute them twice.
Scaling rules:
docker compose up -d --scale worker=4orkubectl scale deployment workerscales all four queues together. To scale one queue independently, run an extra worker container subscribed to a single queue, for example-Q agentto grow AgentRun execution only.- The
sandboxqueue is its own service; scalingsandbox-workerdoes not affect knowledge base or workflow processing. beatalways runs a single replica, see Service topology.
Request routing
An external reverse proxy or Ingress routes requests: /api/* to api:8000, everything else under / to frontend:3000. The frontend image does not include Nginx; deploy/nginx/default.conf is only an optional external example.
Keep the two hops separate:
- Browser → API: the browser calls
NEXT_PUBLIC_API_URL(frontend image build-time default/api/v1, i.e. a relative path), and the reverse proxy or Ingress forwards the/apiprefix toapi:8000. In Compose the frontend service additionally rewrites/api/*toBACKEND_INTERNAL_URLthrough Next.js rewrites. - Next.js server → API:
BACKEND_INTERNAL_URLjoined with the base path. The code default ishttp://localhost:8000; Compose injectshttp://api:8000; the single-file K8s manifest and the default Helmfrontendworkload carry no environment variables.
The backend mounts every router under API_V1_STR, default /api/v1: GET {API_V1_STR}/health and {API_V1_STR}/openapi.json move with it. If you change that value, update the proxy/Ingress path and the frontend build-time NEXT_PUBLIC_API_URL to the same /api/v1 prefix, or both browser and server-side calls will hit a path that does not exist.
Persisted data
Default Compose volumes: postgres_data, redis_data, qdrant_data, and uploads_data. Multiple API replicas using local upload storage require ReadWriteMany; with only ReadWriteOnce, keep a single API replica, while Worker and Sandbox Worker can still scale independently.
Internal addresses
Use service names inside containers, never localhost:
| Variable | Compose / K8s value | Purpose |
|---|---|---|
POSTGRES_SERVER | db / postgres | Database |
REDIS_HOST | redis | Cache and Celery broker/backend |
QDRANT_URL | http://qdrant:6333 | Vector store |
API_BASE_URL | http://api:8000 | Internal API address for server-side use |
API_INTERNAL_BASE_URL | http://api:8000 | Where workers reach the upload gateway (UPLOAD_STORAGE_MODE=remote) |
SANDBOX_ARTIFACT_UPLOAD_BASE_URL | http://api:8000 | Sandbox worker artifact uploads |
In Kubernetes, FRONTEND_URL is http://frontend:3000 (SSO redirect base) and clouisle-secret provides INTERNAL_API_TOKEN, which pods read through INTERNAL_API_TOKEN_FILE=/var/run/secrets/clouisle/internal-api-token. Worker and sandbox-worker mount no uploads volume; they go through this internal gateway instead.
How is this guide?