This commit is contained in:
256
docker-compose.yml
Normal file
256
docker-compose.yml
Normal file
@@ -0,0 +1,256 @@
|
||||
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:
|
||||
Reference in New Issue
Block a user