# Core AI services: Ollama, LiteLLM (with PostgreSQL) and Open WebUI. # # Kit template for guide doc 2, build steps "Ollama", "LiteLLM" and "Open WebUI". Versions as of # October 2026. After the first good start, replace each tag with the digest you tested # (docker inspect --format '{{index .RepoDigests 0}}' ) and update on purpose. # # Every port is published on 127.0.0.1 only: Docker-published ports bypass ufw. # Secrets live in ./.env (chmod 600), created by you on the server, never committed: # POSTGRES_PASSWORD= # LITELLM_MASTER_KEY=sk- LiteLLM requires the sk- prefix # LITELLM_SALT_KEY=sk- never change it after models are stored # WEBUI_SECRET_KEY= # WEBUI_LITELLM_KEY= # Generate random values with: openssl rand -hex 32 name: ai-core x-logging: &logging driver: json-file options: { max-size: "10m", max-file: "3" } services: ollama: image: ollama/ollama:0.40.5 restart: unless-stopped logging: *logging environment: OLLAMA_HOST: 0.0.0.0:11434 # inside the container; the host port below is loopback only OLLAMA_NUM_PARALLEL: "4" # measure first; RAM grows with parallel x context OLLAMA_MAX_LOADED_MODELS: "3" OLLAMA_KEEP_ALIVE: "5m" OLLAMA_CONTEXT_LENGTH: "8192" # set it explicitly; CPU-only defaults are small OLLAMA_FLASH_ATTENTION: "1" OLLAMA_KV_CACHE_TYPE: "q8_0" # about half the KV memory of f16; needs flash attention volumes: - ollama_data:/root/.ollama ports: - "127.0.0.1:11434:11434" networks: [app] cpus: 24 # leave cores for the desktop and builds mem_limit: 48g healthcheck: test: ["CMD", "ollama", "list"] interval: 30s timeout: 10s retries: 10 litellm-db: image: postgres:16 restart: unless-stopped logging: *logging environment: POSTGRES_DB: litellm POSTGRES_USER: litellm POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} volumes: - litellm_db:/var/lib/postgresql/data networks: [database] mem_limit: 2g healthcheck: test: ["CMD-SHELL", "pg_isready -U litellm -d litellm"] interval: 10s timeout: 5s retries: 20 litellm: image: docker.litellm.ai/berriai/litellm:v1.105.1 restart: unless-stopped logging: *logging command: ["--config=/app/config.yaml", "--host=0.0.0.0", "--port=4000"] environment: DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD:?set in .env}@litellm-db:5432/litellm LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?set in .env} LITELLM_SALT_KEY: ${LITELLM_SALT_KEY:?set in .env} STORE_MODEL_IN_DB: "True" LITELLM_TELEMETRY: "False" volumes: - ../litellm/config.yaml:/app/config.yaml:ro ports: - "127.0.0.1:4000:4000" networks: [app, database] mem_limit: 4g depends_on: litellm-db: { condition: service_healthy } ollama: { condition: service_healthy } healthcheck: test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness', timeout=5)"] interval: 15s timeout: 10s start_period: 90s retries: 20 open-webui: image: ghcr.io/open-webui/open-webui:v0.12.2 restart: unless-stopped logging: *logging environment: WEBUI_AUTH: "True" WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:?set in .env} ENABLE_SIGNUP: "False" ENABLE_OLLAMA_API: "False" # chat goes through the gateway, never straight to Ollama ENABLE_OPENAI_API: "True" OPENAI_API_BASE_URL: http://litellm:4000/v1 OPENAI_API_KEY: ${WEBUI_LITELLM_KEY:?set in .env} ENABLE_PERSISTENT_CONFIG: "False" # otherwise values saved at first start override this file ANONYMIZED_TELEMETRY: "False" DO_NOT_TRACK: "true" volumes: - webui_data:/app/backend/data ports: - "127.0.0.1:8080:8080" networks: [app] mem_limit: 12g depends_on: litellm: { condition: service_healthy } volumes: ollama_data: {} litellm_db: {} webui_data: {} networks: app: {} database: internal: true # the database has no route out