services: # --------------------------------------------------------------------- # Infrastructure # --------------------------------------------------------------------- rabbitmq: image: rabbitmq:3.13-management-alpine ports: ["5672:5672", "15672:15672"] environment: RABBITMQ_DEFAULT_USER: enkly RABBITMQ_DEFAULT_PASS: enkly healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 10s timeout: 5s retries: 10 # vLLM OpenAI-compatible inference servers. One container per model — # a vLLM engine process serves exactly one model, so chat (Qwen, # tool-calling + reasoning) and embeddings (Nomic) are two separate # services with independent lifecycles, rather than the two "tags" # one shared Ollama daemon used to serve. Both need an NVIDIA GPU and # the nvidia container runtime (`docker info | grep nvidia` to check) # — on a CPU-only dev box, run `docker compose --profile cpu up` isn't # provided here because vLLM's CPU backend is a materially different # build/image; see https://docs.vllm.ai for that path if you need it. # # Pin the image tag to a version you've actually tested — "latest" on # a GPU inference server is how a routine `docker compose pull` # becomes an unplanned model-serving outage. v0.31.0 below is this # deployment's current pin (matches k8s/11-vllm.yaml); bump # deliberately. Must stay >= the first tag with Qwen3.5 model # support or the server fails to load with an unrecognized # architecture error. vllm-chat: image: vllm/vllm-openai:v0.31.0 command: - --model=Qwen/Qwen3.5-35B-A3B-GPTQ-Int4 - --served-model-name=${ENKLY_CHAT_MODEL:-qwen3.5} - --quantization=gptq_marlin - --tool-call-parser=qwen3_coder - --enable-auto-tool-choice - --reasoning-parser=qwen3 - --max-model-len=32768 - --gpu-memory-utilization=0.90 - --tensor-parallel-size=${ENKLY_CHAT_TP_SIZE:-1} - --api-key=${ENKLY_VLLM_API_KEY:-} ports: ["8000:8000"] volumes: ["hf-cache:/root/.cache/huggingface"] environment: HUGGING_FACE_HUB_TOKEN: "${HUGGING_FACE_HUB_TOKEN:-}" deploy: resources: reservations: devices: - driver: nvidia count: ${ENKLY_CHAT_TP_SIZE:-1} capabilities: [gpu] # vLLM's own /dev/shm requirement for NCCL/CUDA-graph scratch space # — without this, multi-GPU (tensor-parallel) startup or even # single-GPU CUDA-graph capture can fail or silently fall back to a # slower path. 2gb is generous for a single-GPU chat replica; raise # it if ENKLY_CHAT_TP_SIZE > 1. shm_size: "2gb" healthcheck: test: ["CMD", "python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"] # Model load (Qwen3.5-35B-A3B-GPTQ-Int4, ~20GB on disk) can still # take a few minutes from a cold Hugging Face cache — a short # interval just means more log noise while it warms up, not a # faster start. interval: 15s timeout: 5s retries: 40 start_period: 10m vllm-embed: image: vllm/vllm-openai:v0.6.3 command: - --model=nomic-ai/nomic-embed-text-v1.5 - --served-model-name=${ENKLY_EMBED_MODEL:-nomic-embed-text} - --task=embed - --trust-remote-code - --gpu-memory-utilization=0.30 ports: ["8001:8000"] volumes: ["hf-cache:/root/.cache/huggingface"] environment: HUGGING_FACE_HUB_TOKEN: "${HUGGING_FACE_HUB_TOKEN:-}" deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] shm_size: "1gb" healthcheck: test: ["CMD", "python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"] interval: 10s timeout: 5s retries: 30 start_period: 3m # Database-per-service: four separate Postgres databases, never one # shared instance a service could accidentally cross-query. knowledge # uses the pgvector image; the other three a bare postgres image. iam-db: image: postgres:16-alpine environment: {POSTGRES_USER: enkly, POSTGRES_PASSWORD: enkly, POSTGRES_DB: enkly_iam} volumes: ["iam-db-data:/var/lib/postgresql/data"] healthcheck: {test: ["CMD-SHELL", "pg_isready -U enkly"], interval: 5s, timeout: 5s, retries: 10} billing-db: image: postgres:16-alpine environment: {POSTGRES_USER: enkly, POSTGRES_PASSWORD: enkly, POSTGRES_DB: enkly_billing} volumes: ["billing-db-data:/var/lib/postgresql/data"] healthcheck: {test: ["CMD-SHELL", "pg_isready -U enkly"], interval: 5s, timeout: 5s, retries: 10} conversation-db: image: postgres:16-alpine environment: {POSTGRES_USER: enkly, POSTGRES_PASSWORD: enkly, POSTGRES_DB: enkly_conversation} volumes: ["conversation-db-data:/var/lib/postgresql/data"] healthcheck: {test: ["CMD-SHELL", "pg_isready -U enkly"], interval: 5s, timeout: 5s, retries: 10} knowledge-db: image: pgvector/pgvector:pg16 environment: {POSTGRES_USER: enkly, POSTGRES_PASSWORD: enkly, POSTGRES_DB: enkly_knowledge} volumes: ["knowledge-db-data:/var/lib/postgresql/data"] healthcheck: {test: ["CMD-SHELL", "pg_isready -U enkly"], interval: 5s, timeout: 5s, retries: 10} # --------------------------------------------------------------------- # Application services # --------------------------------------------------------------------- iam: build: {context: ., dockerfile: services/iam/Dockerfile} depends_on: iam-db: {condition: service_healthy} rabbitmq: {condition: service_healthy} environment: ENKLY_IAM_GRPC_ADDR: ":9001" ENKLY_IAM_POSTGRES_DSN: "postgres://enkly:enkly@iam-db:5432/enkly_iam?sslmode=disable" ENKLY_RABBITMQ_URL: "amqp://enkly:enkly@rabbitmq:5672/" ENKLY_BASE_URL: "${ENKLY_BASE_URL:-http://localhost:8080}" ENKLY_SMTP_USERNAME: "${ENKLY_SMTP_USERNAME:-}" ENKLY_SMTP_PASSWORD: "${ENKLY_SMTP_PASSWORD:-}" restart: unless-stopped billing: build: {context: ., dockerfile: services/billing/Dockerfile} depends_on: billing-db: {condition: service_healthy} rabbitmq: {condition: service_healthy} environment: ENKLY_BILLING_GRPC_ADDR: ":9002" ENKLY_BILLING_POSTGRES_DSN: "postgres://enkly:enkly@billing-db:5432/enkly_billing?sslmode=disable" ENKLY_RABBITMQ_URL: "amqp://enkly:enkly@rabbitmq:5672/" STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:-}" STRIPE_PUBLISH_KEY: "${STRIPE_PUBLISH_KEY:-}" STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:-}" STRIPE_PRICE_BASIC: "${STRIPE_PRICE_BASIC:-}" STRIPE_PRICE_PRO: "${STRIPE_PRICE_PRO:-}" STRIPE_PRICE_PREMIUM: "${STRIPE_PRICE_PREMIUM:-}" restart: unless-stopped conversation: build: {context: ., dockerfile: services/conversation/Dockerfile} depends_on: conversation-db: {condition: service_healthy} rabbitmq: {condition: service_healthy} environment: ENKLY_CONVERSATION_GRPC_ADDR: ":9003" ENKLY_CONVERSATION_POSTGRES_DSN: "postgres://enkly:enkly@conversation-db:5432/enkly_conversation?sslmode=disable" ENKLY_RABBITMQ_URL: "amqp://enkly:enkly@rabbitmq:5672/" restart: unless-stopped workspace: build: {context: ., dockerfile: services/workspace/Dockerfile} volumes: ["workspace-data:/data/workspace"] environment: ENKLY_WORKSPACE_GRPC_ADDR: ":9004" ENKLY_WORKSPACE: "/data/workspace" ENKLY_BASH_TIMEOUT_SECONDS: "${ENKLY_BASH_TIMEOUT_SECONDS:-0}" ENKLY_BASH_ALLOWLIST: "${ENKLY_BASH_ALLOWLIST:-}" restart: unless-stopped knowledge: build: {context: ., dockerfile: services/knowledge/Dockerfile} depends_on: knowledge-db: {condition: service_healthy} vllm-embed: {condition: service_healthy} environment: ENKLY_KNOWLEDGE_GRPC_ADDR: ":9005" ENKLY_KNOWLEDGE_POSTGRES_DSN: "postgres://enkly:enkly@knowledge-db:5432/enkly_knowledge?sslmode=disable" ENKLY_VLLM_EMBED_URL: "https://vllm-embed.enkly.org/v1" ENKLY_EMBED_MODEL: "${ENKLY_EMBED_MODEL:-nomic-embed-text}" ENKLY_VLLM_API_KEY: "${ENKLY_VLLM_API_KEY:-}" restart: unless-stopped agent: build: {context: ., dockerfile: services/agent/Dockerfile} depends_on: workspace: {condition: service_started} knowledge: {condition: service_started} billing: {condition: service_started} conversation: {condition: service_started} vllm-chat: {condition: service_healthy} vllm-embed: {condition: service_healthy} environment: ENKLY_AGENT_GRPC_ADDR: ":9006" ENKLY_WORKSPACE_GRPC_ADDR: "workspace:9004" ENKLY_KNOWLEDGE_GRPC_ADDR: "knowledge:9005" ENKLY_BILLING_GRPC_ADDR: "billing:9002" ENKLY_CONVERSATION_GRPC_ADDR: "conversation:9003" ENKLY_VLLM_CHAT_URL: "https://vllm-chat.enkly.org/v1" ENKLY_VLLM_EMBED_URL: "https://vllm-embed.enkly.org/v1" ENKLY_CHAT_MODEL: "${ENKLY_CHAT_MODEL:-qwen3.5}" ENKLY_EMBED_MODEL: "${ENKLY_EMBED_MODEL:-nomic-embed-text}" ENKLY_VLLM_API_KEY: "${ENKLY_VLLM_API_KEY:-}" restart: unless-stopped notification: build: {context: ., dockerfile: services/notification/Dockerfile} depends_on: rabbitmq: {condition: service_healthy} environment: ENKLY_RABBITMQ_URL: "amqp://enkly:enkly@rabbitmq:5672/" ENKLY_SMTP_HOST: "${ENKLY_SMTP_HOST:-smtp.gmail.com}" ENKLY_SMTP_PORT: "${ENKLY_SMTP_PORT:-587}" ENKLY_SMTP_USERNAME: "${ENKLY_SMTP_USERNAME:-}" ENKLY_SMTP_PASSWORD: "${ENKLY_SMTP_PASSWORD:-}" ENKLY_SMTP_FROM: "${ENKLY_SMTP_FROM:-}" restart: unless-stopped gateway: build: {context: ., dockerfile: services/gateway/Dockerfile} depends_on: [iam, billing, conversation, workspace, knowledge, agent] ports: ["8080:8080"] environment: ENKLY_GATEWAY_HTTP_ADDR: ":8080" ENKLY_BASE_URL: "${ENKLY_BASE_URL:-http://localhost:8080}" ENKLY_IAM_GRPC_ADDR: "iam:9001" ENKLY_BILLING_GRPC_ADDR: "billing:9002" ENKLY_CONVERSATION_GRPC_ADDR: "conversation:9003" ENKLY_WORKSPACE_GRPC_ADDR: "workspace:9004" ENKLY_KNOWLEDGE_GRPC_ADDR: "knowledge:9005" ENKLY_AGENT_GRPC_ADDR: "agent:9006" STRIPE_PUBLISH_KEY: "${STRIPE_PUBLISH_KEY:-}" restart: unless-stopped volumes: hf-cache: iam-db-data: billing-db-data: conversation-db-data: knowledge-db-data: workspace-data: