Skip to main content

CubePlex on Docker Compose

docker compose up -d deploys CubePlex (backend + frontend + Postgres + Redis + rustfs S3 store) on a single host. It uses the same container images as the Kubernetes deployment mode — only the orchestration differs.

1. Prerequisites

ItemRequirement
Linux host with Docker engine≥ 24, with docker compose v2
Outbound network to pull imagesghcr.io and Docker Hub (or your own mirror)
LLM provider credentialsat least one — see LLM provider configuration
Open ports on the hostone for the frontend (default 3000), optionally one for the backend (default 8000)

No Kubernetes, no Helm.

2. Architecture

Host
├─ port :3000 → frontend (Next.js) ── proxies /api/* server-side ──┐
│ │
└─ port :8000 → backend (FastAPI / uvicorn) ◄─────────────────────┘
├─ depends on → postgres (named volume)
├─ depends on → redis (named volume)
└─ depends on → rustfs (S3 store, named volume)

Bootstrap services (run-to-completion):
backend-migrate alembic upgrade head (gates backend boot)
bucket-init mc mb (idempotent rustfs bucket create)

All inter-service communication uses Docker DNS (for example, the backend reaches Postgres at postgres:5432). The host only sees the frontend port (and optionally the backend port, for direct API access).

3. Choose images

The compose stack defaults to the public prebuilt release images on GHCR — no build step required:

ghcr.io/cubeplexai/cubeplex-backend:<version>
ghcr.io/cubeplexai/cubeplex-frontend:<version>

Pick a version tag from the releases page — backend and frontend share one app version, e.g. v0.7.2 — and set it as BACKEND_TAG / FRONTEND_TAG in the next step. IMAGE_REGISTRY / IMAGE_REPO already default to ghcr.io / cubeplexai, so you don't need to change them for a standard install. GHCR release images are public — no docker login needed.

Build your own images instead (private registry / air-gapped / patched)

Use the Kubernetes mode's build script — the backend and frontend images are identical either way:

deploy/kubernetes/scripts/build-and-push.sh
# pushes to ${REGISTRY}/${REPO}/cubeplex-{backend,frontend}:<YYMMDD>-<branch>-<short-sha>

Then point IMAGE_REGISTRY, IMAGE_REPO, BACKEND_TAG, and FRONTEND_TAG in .env at that build.

4. Configure (.env + two YAML files)

Three files, all gitignored:

FileWhat it does
.envImage tags, host port mappings, infra passwords. Read directly by docker compose for variable substitution in compose.yaml.
config/config.production.local.yamlNon-secret runtime config (mode, public URL, cookie security, sandbox toggle). Mounted into the backend.
config/config.production.secrets.yamlSecrets — JWT/CSRF/vault material, infra passwords (must match .env), LLM provider API keys. Mounted into the backend.

This section covers the keys needed to boot. For every backend config key, the layering rules, and the env-var mapping, see the backend configuration reference.

4.1 .env

cp .env.example .env
$EDITOR .env

Required:

IMAGE_REGISTRY=ghcr.io
IMAGE_REPO=cubeplexai
BACKEND_TAG=v0.7.2 # a release version from the releases page
FRONTEND_TAG=v0.7.2

# openssl rand -hex 16
POSTGRES_PASSWORD=<...>
REDIS_PASSWORD=<...>
RUSTFS_SECRET_KEY=<...>

Optional (defaults shown):

BACKEND_PORT=8000
FRONTEND_PORT=3000
POSTGRES_USER=cubeplex
POSTGRES_DB=cubeplex
RUSTFS_ACCESS_KEY=cubeplex
OBJECTSTORE_BUCKET=cubeplex

4.2 config.production.local.yaml

cp config/config.production.local.yaml.example config/config.production.local.yaml
$EDITOR config/config.production.local.yaml
FieldDefaultNotes
api.public_urlhttp://localhost:8000The URL clients reach the backend at; if you put a reverse proxy in front, use that URL.
public_base_urlsameUsed by the backend for absolute URL construction.
frontend_base_urlhttp://localhost:3000Where the backend redirects browsers.
deployment.modesingle_tenantsingle_tenant auto-creates an org on first user registration; multi_tenant requires explicit org bootstrap.
auth.cookie_securefalseMust stay false on plain HTTP — otherwise clients silently drop the auth cookie.
sandbox.enabledfalseFlip to true and fill sandbox.{domain,image,api_key} in secrets.yaml to use an external OpenSandbox. See Optional: sandbox execution below.
note

database.host, redis.url, and objectstore.endpoint use Docker DNS names (postgres, redis, rustfs) — don't change them unless you renamed the services.

4.3 config.production.secrets.yaml

cp config/config.production.secrets.yaml.example config/config.production.secrets.yaml
$EDITOR config/config.production.secrets.yaml

Required:

production:
auth:
jwt_secret: "<openssl rand -hex 32>"
csrf_secret: "<openssl rand -hex 32>"
vault_key: "<Fernet.generate_key()>"
database:
password: "<same as POSTGRES_PASSWORD>"
redis:
url: "redis://:<REDIS_PASSWORD>@redis:6379/0"
objectstore:
access_key: "cubeplex" # same as RUSTFS_ACCESS_KEY
access_secret: "<RUSTFS_SECRET_KEY>"

See Required secrets for what jwt_secret, csrf_secret, and vault_key are for and how to generate them.

4.4 LLM providers

Configured under production.llm in config.production.secrets.yaml — see LLM provider configuration for the full field reference and examples.

5. Up / down / logs

# bring up (also pulls the latest tags)
deploy/docker-compose/scripts/up.sh

# tail logs
docker compose -f deploy/docker-compose/compose.yaml logs -f backend

# stop and remove containers (volumes preserved)
docker compose -f deploy/docker-compose/compose.yaml down
warning

docker compose -f deploy/docker-compose/compose.yaml down -v stops and deletes volumes (Postgres data, rustfs data, Redis data) — destructive, use only when you intend to wipe the deployment.

up.sh refuses to start if .env or either YAML config file is missing.

6. Verification

# fast health-only check
deploy/docker-compose/scripts/smoke-test.sh

# end-to-end including a real LLM call
PROMPT="Say the word hello and nothing else." \
deploy/docker-compose/scripts/e2e.sh

e2e.sh drives:

register → login → resolve workspace → create conversation
→ POST message → SSE stream → assert text_delta arrived

On a fresh deployment the first registered user has no workspace yet, so the script runs onboarding to create one; later users already have one from registration.

Both scripts default to localhost; override with HOST, BACKEND_PORT, FRONTEND_PORT to run against a remote host.

7. Troubleshooting

Backend keeps restarting

docker compose -f deploy/docker-compose/compose.yaml logs backend --tail=50
SymptomFix
CUBEPLEX_AUTH__VAULT_KEY is requiredAdd auth.vault_key in secrets.yaml.
connection refused on postgres:5432Postgres is still starting; should self-heal — check docker compose ps.
Provider 'X' not founddefault_model: "X/..." references a provider not listed under providers.

config.production.local.yaml's auth.cookie_secure must be false — otherwise the browser (or curl) silently drops the auth cookie because the connection is plain HTTP.

Frontend → backend fails (CORS / 502)

compose.yaml sets CUBEPLEX_API_URL=http://backend:8000 on the frontend container, so Next.js proxies /api/* server-side over the Docker network. If you changed service names, update that env var too.

Image pull fails

The default GHCR release images are public — no login needed. Check that BACKEND_TAG / FRONTEND_TAG name a real release tag (see the releases page); a manifest unknown / not found error means the tag doesn't exist.

If you pointed IMAGE_REGISTRY at a private mirror instead:

docker login ${IMAGE_REGISTRY}

The compose stack inherits the Docker daemon's credentials.

Stuck bucket-init

docker compose -f deploy/docker-compose/compose.yaml logs bucket-init

If rustfs isn't reachable, check the rustfs container's healthcheck — rustfs publishes a console on :9001 you can hit locally to confirm it's up.

Optional: sandbox execution (OpenSandbox)

CubePlex executes agent tool calls (bash, file read/write, …) inside a sandbox. Without it, chat still works but tool calls fail. This section covers deploying alibaba's OpenSandbox lifecycle server in Docker runtime mode alongside the compose stack.

If you only need CubePlex chat without agent tool calls, skip this section and leave sandbox.enabled: false in config.production.local.yaml.

What the overlay deploys

The optional compose.opensandbox.yaml overlay adds one service:

opensandbox-server image: opensandbox/server:latest
mounts: /var/run/docker.sock
reads: /etc/opensandbox/config.toml
port: 8090

The OpenSandbox server is a normal Python/FastAPI container. When it receives POST /sandboxes, it talks to the host Docker daemon via the mounted socket to spawn sibling sandbox containers (not nested) — they run on the same Docker engine as CubePlex, on a separate bridge network.

danger

Anything inside the opensandbox-server container can effectively root the host via the Docker socket. Keep it on your private network — don't expose port 8090 publicly.

Quickstart

cd deploy/docker-compose

# 1. OpenSandbox config (gitignored)
cp config/opensandbox.toml.example config/opensandbox.toml
$EDITOR config/opensandbox.toml # set api_key, eip/host_ip, execd_image, egress.image

# 2. backend secrets — sandbox section
$EDITOR config/config.production.secrets.yaml
# sandbox:
# domain: "opensandbox-server:8090" # Docker DNS name from this overlay
# image: "ghcr.io/cubeplexai/cubeplex-sandbox:v0.7.2"
# api_key: "<same as [server].api_key in opensandbox.toml>"

# 3. backend non-secret — enable sandbox
$EDITOR config/config.production.local.yaml
# sandbox:
# enabled: true
# secure_access: false # docker runtime rejects secureAccess=True
# use_server_proxy: false # OpenSandbox v0.1.x drops the port from
# # proxied URLs; use direct host-mapped ports

# 4. up with the overlay
docker compose \
-f compose.yaml \
-f compose.opensandbox.yaml \
up -d

Operator-managed values (no template):

KeyWhereNotes
opensandbox.toml [server].api_keyconfig/opensandbox.tomlRequired; must match sandbox.api_key in CubePlex secrets.
opensandbox.toml [server].eipsameHost/IP returned to CubePlex in endpoint URLs; usually host.docker.internal.
opensandbox.toml [runtime].execd_imagesameImage carrying the execd binary; pull-reachable by the host Docker daemon.
opensandbox.toml [egress].imagesameEgress sidecar image; required because CubePlex always sends a network policy.
opensandbox.toml [docker].network_modesameMust be bridge for CubePlex (see the compatibility matrix below).

Compatibility — CubePlex features under Docker-mode OpenSandbox

Docker runtime mode has real limitations compared to Kubernetes-mode OpenSandbox. This matrix reflects opensandbox-server v0.1.14.

Secure-access toggle: the Docker runtime rejects secureAccess=True with HTTP 400 — secured endpoints are a Kubernetes ingress-gateway feature. CubePlex exposes a sandbox.secure_access config knob that defaults to true (matching Kubernetes-mode behavior); the compose mode's example config sets it to false, so CubePlex sends secureAccess: false and the Docker runtime accepts the request. With that flag set, chat → sandbox tool call → tool_result works end to end.

FeatureWhat worksWhat doesn't
Chat → agent tool calls (bash / file read-write in the sandbox)Yes — chat → sandbox executetool_result works end to end
Network policy (egress firewall)Yes — the egress sidecar is created and enforces the policy in DNS mode, but only when [docker].network_mode = "bridge"Rejected when network_mode=host or a user-defined bridge network
File-tree panel (chat UI)Yes
Interactive terminal panel (chat UI)Yes — proxied through the backend's panel routeRequires api.public_url set to a URL the user's browser can reach that also forwards WebSocket
Live browser panel (Neko, chat UI)Yes — same panel route as the terminalSame api.public_url requirement
Secret injection / env substitution (cbxref_… placeholders)❌ Not available. It needs the backend mTLS credential-exchange listener + egress CA/client certs that the Kubernetes chart deploys (gen-egress-certs.sh + the egress webhook). The compose overlay ships none of that, and sandbox.egress_exchange_host stays empty, so injection is disabled.
Server-proxy mode (use_server_proxy: true)OpenSandbox v0.1.x drops the port from the proxied endpoint URL. Keep use_server_proxy: false (the example default), and the overlay wires host.docker.internal via extra_hosts on both the backend and the opensandbox-server so they can reach the host-mapped bridge ports of sandbox containers.
pvc.claimName volumesYes — but treated as Docker named volumesNo CSI features, no ReadWriteMany
Pause / resume (POST /sandboxes/{id}/pause, etc.)Calls Docker pause/unpause (cgroup freezer)No checkpoint to disk — paused state is lost on host Docker restart. CubePlex defaults pause_on_idle: false for this reason.

In short: under Docker-mode OpenSandbox, the agent can run commands, read/write files, browse the file tree, and open the interactive terminal and live browser panels — all through the backend's panel reverse proxy, so set api.public_url to a browser-reachable, WebSocket-forwarding backend URL. Only secret injection still requires the Kubernetes runtime; for that, deploy via the Kubernetes guide.

The following routes also return 501 Not Implemented on Docker runtime, even though they exist in the OpenAPI spec (CubePlex doesn't call any of them today): POST /pools and related pre-warmed pod pools, and the snapshot APIs (POST /sandboxes/{id}/snapshots, etc.) — both are Kubernetes-only.

Verifying

docker compose -f compose.yaml -f compose.opensandbox.yaml ps
# expect: opensandbox-server Up (healthy)

Direct API probe (from inside the backend container, using Docker DNS):

docker exec cubeplex-backend-1 python -c "
import urllib.request, json
req = urllib.request.Request(
'http://opensandbox-server:8090/sandboxes',
headers={'OPEN-SANDBOX-API-KEY': '<your api_key>'},
)
print(urllib.request.urlopen(req, timeout=5).read().decode())
"
# expect: {"items":[], ...}

End-to-end (CubePlex chat → sandbox tool call) works once config.production.local.yaml has sandbox.enabled: true and sandbox.secure_access: false and sandbox.use_server_proxy: false. A prompt like ls -la /workspace should produce a real tool_result containing the sandbox filesystem contents.

Tearing down

docker compose -f compose.yaml -f compose.opensandbox.yaml down
# This also removes the CubePlex stack. Use `down opensandbox-server`
# to remove only the overlay's service.

The MITM CA and any sandbox containers spawned by the server stay on the host Docker engine — they aren't part of this project's compose network. Inspect with docker ps --filter "name=sandbox-".

Optional: document parsing (docling-serve)

The backend's read tool converts uploaded PDF / office documents to markdown by calling a docling-serve instance. Without it, other file types still work but document parsing doesn't. The optional compose.docling.yaml overlay supports two deployment shapes.

Combined: same host, same Docker network

cd deploy/docker-compose

docker compose \
-f compose.yaml \
-f compose.docling.yaml \
--profile cpu \
up -d

Backend reaches it as docling-serve-cpu:5001 over Docker DNS — no manual network bridging needed. Use --profile gpu instead for the CUDA image (docling-serve-cu130, requires the NVIDIA container runtime on the host).

Standalone: a separate host

Copy just compose.docling.yaml to its own host — for example a dedicated GPU box shared by multiple projects — and run it there on its own:

docker compose -f compose.docling.yaml --profile gpu up -d

Then point CubePlex's backend at that host from wherever it runs.

Configure the backend

Either way, always pass --profile cpu or --profile gpu — neither docling-serve-* service starts without one (the model-download job runs for either). Set the resulting URL in config.production.local.yaml:

parsers:
docling_serve:
base_url: "http://docling-serve-cpu:5001" # combined, --profile cpu
# base_url: "http://docling-serve-cu130:5001" # combined, --profile gpu
# base_url: "http://<standalone host>:<port>" # standalone deployment

Model download and registries

The docling-models service downloads the model set (layout, table former, OCR, and VLM models — several GB) into a named volume on first start, and reuses it on restart. Both docling-serve-cpu and docling-serve-cu130 wait for it to finish before serving.

If the default GHCR registry or HuggingFace are slow or blocked from your build host, override before starting:

# Alternate image registry (quay.io mirror, or a China mainland sync — verify before production use)
export DOCLING_REGISTRY=quay.io/docling-project
# or: export DOCLING_REGISTRY=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/docling-project

# HuggingFace mirror for model downloads
export HF_ENDPOINT=https://hf-mirror.com
# HF_TOKEN=hf_xxx # only needed for gated/private repos