Skip to content

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

  1. Install Docker as described in the requirements and get a license token from the cabinet.

  2. Create an install directory (for example /root/copula) and enter it. The data, workspace, and ssh subdirectories are created automatically by the compose volumes on first start:

    bash
    mkdir -p /root/copula && cd /root/copula
  3. Create docker-compose.yml with the following content:

    yaml
    version: '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:
  4. Create .env with 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:full

    Required values to fill in:

    • JWT_SECRET — exactly 128 hex characters; generate with openssl rand -hex 64
    • ENCRYPTION_KEY — exactly 32 characters; generate with openssl rand -hex 16
    • POSTGRES_PASSWORD — a strong password for the bundled PostgreSQL
    • LICENSE_TOKEN — the token from the cabinet
    • ADMIN_USER / ADMIN_PASS — initial admin credentials

    See the full environment variable reference for every variable.

  5. Pull the images and start the stack:

    bash
    docker compose pull
    docker compose up -d
  6. Verify that migrations applied and the backend is running:

    bash
    docker logs copula-migrate
    docker logs copula-back

What the stack contains

ServiceRole
copula-postgresPostgreSQL (postgres:16-alpine); internal only, no host port
copula-migrateInit 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-backThe backend (njoy14/copula-api:latest); host port 8901${BACKEND_PORT}; mounts workspace, ssh, .env, and the Docker socket
copula-cocoindexCode-search daemon (cocoindex/cocoindex-code:full)
copula-caddyOptional 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:

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