Manual installation
Manual installation is the alternative to the setup wizard: you run the published backend image njoy14/copula-api:latest with your own docker-compose.yml. No repository access, cloning, or building from source is needed — everything runs from public images. Use it when you want full control over the compose stack.
Prerequisites: a server that meets the requirements (Docker installed) and a license token.
WARNING
The default stack has no TLS and no reverse proxy. Put your own reverse proxy in front of the backend — see Reverse proxy and TLS.
Steps
Install Docker as described in the requirements and get a license token from the cabinet.
Create an install directory (for example
/root/copula) and enter it. Thedata,workspace, andsshsubdirectories are created automatically by the compose volumes on first start:bashmkdir -p /root/copula && cd /root/copulaCreate
docker-compose.ymlwith the following content:yamlversion: '3.9' services: copula-postgres: container_name: copula-postgres image: postgres:16-alpine restart: unless-stopped networks: - copula-network environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - ./data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 copula-migrate: container_name: copula-migrate image: njoy14/copula-api:latest entrypoint: ["/usr/local/bin/migrate"] networks: - copula-network env_file: - .env command: - "-path=/migrations" - "-database" - "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@copula-postgres:5432/${POSTGRES_DB}?sslmode=disable" - "up" restart: "no" depends_on: copula-postgres: condition: service_healthy copula-back: container_name: copula-back image: njoy14/copula-api:latest restart: unless-stopped user: "root" networks: - copula-network volumes: - ./workspace:/workspace - ./ssh:/root/.ssh - ./.env:/config/.env:ro - /var/run/docker.sock:/var/run/docker.sock ports: - "8901:${BACKEND_PORT}" env_file: - .env depends_on: copula-postgres: condition: service_healthy copula-migrate: condition: service_completed_successfully copula-cocoindex: container_name: copula-cocoindex image: ${COCOINDEX_CODE_IMAGE:-cocoindex/cocoindex-code:full} restart: unless-stopped volumes: - ./workspace:/workspace - cocoindex-data:/var/cocoindex stdin_open: true tty: true command: ccc mcp healthcheck: test: ["CMD-SHELL", "ccc doctor || exit 1"] interval: 10s timeout: 5s retries: 3 start_period: 30s # Optional TLS terminator. Uncomment ONLY if this host has a public # domain with DNS already pointing at it and ports 80/443 free. # Otherwise keep it commented and follow the "Reverse proxy and TLS" page. # copula-caddy: # container_name: copula-caddy # image: caddy:2.11.4-alpine # restart: unless-stopped # networks: # - copula-network # ports: # - "80:80" # - "443:443" # volumes: # - ./Caddyfile:/etc/caddy/Caddyfile:ro # - ./caddy-data:/data # depends_on: # - copula-back volumes: cocoindex-data: networks: copula-network:Create
.envwith the following content and fill in the required values:bash# === Backend === BACKEND_PORT=8080 LOG_LEVEL=info CORS_ALLOWED_ORIGINS=* PUBLIC_BASE_URL=https://copula.example.com # === Security (all required) === # exactly 128 hex chars — generate: openssl rand -hex 64 JWT_SECRET= # exactly 32 chars — generate: openssl rand -hex 16 ENCRYPTION_KEY= # license token from the cabinet https://app.copul4.com LICENSE_TOKEN= CABINET_URL=https://api.copul4.com # === PostgreSQL (matches the copula-postgres service) === POSTGRES_HOST=copula-postgres POSTGRES_PORT=5432 POSTGRES_USER=agent POSTGRES_PASSWORD= POSTGRES_DB=agentdb # === Initial admin account === ADMIN_USER=admin ADMIN_PASS= # === Model (default: cabinet LLM router; license token is the credential) === MODEL_PROVIDER=copula MODEL_BASE_URL= MODEL_NAME=copula-base # MODEL_API_KEY= # not needed for provider=copula # SMART_MODEL_* / BASIC_MODEL_* — optional per-profile overrides, see env-reference # === Agent runtime === AGENT_WORKSPACE=/workspace AGENT_CONTEXT_TOKEN_LIMIT=120000 MAX_REPO_WORKERS=20 # === Storage === SESSION_STORAGE_TYPE=database SESSION_FILE_PATH=./sessions FILE_STORAGE_PATH=./uploads MAX_FILE_SIZE_MB=200 # === Integrations === SP_DISTRIBUTOR_URL=https://get.copul4.com COCOINDEX_CODE_IMAGE=cocoindex/cocoindex-code:fullRequired values to fill in:
JWT_SECRET— exactly 128 hex characters; generate withopenssl rand -hex 64ENCRYPTION_KEY— exactly 32 characters; generate withopenssl rand -hex 16POSTGRES_PASSWORD— a strong password for the bundled PostgreSQLLICENSE_TOKEN— the token from the cabinetADMIN_USER/ADMIN_PASS— initial admin credentials
See the full environment variable reference for every variable.
Pull the images and start the stack:
bashdocker compose pull docker compose up -dVerify that migrations applied and the backend is running:
bashdocker logs copula-migrate docker logs copula-back
What the stack contains
| Service | Role |
|---|---|
copula-postgres | PostgreSQL (postgres:16-alpine); internal only, no host port |
copula-migrate | Init job from njoy14/copula-api:latest (entrypoint /usr/local/bin/migrate, migrations baked into the image at /migrations); applies database migrations before the backend starts |
copula-back | The backend (njoy14/copula-api:latest); host port 8901 → ${BACKEND_PORT}; mounts workspace, ssh, .env, and the Docker socket |
copula-cocoindex | Code-search daemon (cocoindex/cocoindex-code:full) |
copula-caddy | Optional TLS terminator (caddy:2.11.4-alpine); commented out by default |
On first start, the backend's entrypoint.sh generates an RSA SSH key into /root/.ssh inside the container (persisted in ./ssh on the host).
INFO
Enabling the commented copula-caddy block requires a public domain with DNS already resolving to this host and ports 80/443 free. You also need a Caddyfile next to the compose file:
copula.example.com {
reverse_proxy copula-back:8080
}The port in reverse_proxy must equal BACKEND_PORT from .env — the Caddyfile has no environment variable interpolation. The domain's A record must resolve before docker compose up, otherwise certificate issuance fails.
After the install
The backend listens on host port 8901. Add a reverse proxy with TLS in front of it (Reverse proxy and TLS), then add the resulting https://<domain> URL as a Space in the Copula app.