odysseus Docker & Self-Hosted Deployment

GitHub

Self-hosted AI workspace.

85,261 stars Python Python 1 file ยท ~1,569 tokens
RAW Compose
1. Pull Image (DOCKERHUB) docker pull docker.io/chromadb/chroma
2. Docker Run (1-Click Launch) docker run -d --name odysseus -p 7011:7011 --restart always docker.io/chromadb/chroma:latest
3. Podman Run podman run -d --name odysseus -p 7011:7011 --restart always docker.io/chromadb/chroma:latest
4. Skopeo Inspect (Zero-Pull Manifest) skopeo inspect docker://docker.io/docker.io/chromadb/chroma:latest
5. ORAS Pull (OCI Artifact) oras pull docker.io/chromadb/chroma:latest
6. Compose Deployment docker compose up -d
docker-compose.gpu-amd.yml
Download .yml
# Standalone AMD ROCm GPU Compose file for stack-management UIs (Portainer,
# Coolify, Dockhand, etc.) that accept only a single Compose file and do not
# reliably honor COMPOSE_FILE or multiple `-f` overlays.
#
# This is equivalent to: docker-compose.yml + docker/gpu.amd.yml.
# The base docker-compose.yml plus the docker/gpu.amd.yml overlay remain the
# source of truth โ€” CLI users should keep using the COMPOSE_FILE overlay
# workflow. Keep this file in sync with both when either changes.
#
# Requires ROCm drivers on the host (kfd + DRI devices) and the host user
# running Docker in the `video` and `render` groups. Set RENDER_GID to your
# host's numeric render group id when needed. See docker/gpu.amd.yml for details.
services:
  odysseus:
    # Official multi-arch GHCR image (linux/amd64 + linux/arm64), published by
    # the "ci / docker publish" workflow on every push to main and dev.
    # Docker pulls this image when it is reachable, and only falls back to the
    # local build below when the pull fails (e.g. no network on the host), so
    # hosts without a build toolchain (Portainer stacks, etc.) get the
    # registry build. For production, pin an immutable tag via ODYSSEUS_IMAGE
    # - e.g. ghcr.io/odysseus-dev/odysseus:1.0.2-7c8070f (X.Y.Z-<sha>) - since
    # :latest and bare :X.Y.Z tags move on every main push.
    image: ${ODYSSEUS_IMAGE:-ghcr.io/odysseus-dev/odysseus:latest}
    build: .
    ports:
      - "${APP_BIND:-127.0.0.1}:${APP_PORT:-7011}:7000"
    volumes:
      - ${APP_DATA_DIR:-./data}:/app/data:z
      - ${APP_LOGS_DIR:-./logs}:/app/logs:z
      # Cookbook remote-server SSH identity. Odysseus can generate a key here;
      # add the shown public key to each remote server's authorized_keys.
      - ${APP_DATA_DIR:-./data}/ssh:/app/.ssh:z
      # Cookbook local model cache. Inside Docker, "Local" means the Odysseus
      # container, so persist its HuggingFace cache under ./data/huggingface.
      - ${APP_DATA_DIR:-./data}/huggingface:/app/.cache/huggingface:z
      # Cookbook-installed Python CLIs/packages (vLLM, llama-cpp-python, etc.)
      # land under /app/.local for the odysseus user. Persist them so a
      # container recreate does not silently remove installed serve engines.
      - ${APP_DATA_DIR:-./data}/local:/app/.local:z
    extra_hosts:
      # Lets the container reach local services on the Docker host, including
      # Ollama at http://host.docker.internal:11434.
      - "host.docker.internal:host-gateway"
    environment:
      - LLM_HOST=${LLM_HOST:-localhost}
      - LLM_HOSTS=${LLM_HOSTS:-}
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}
      - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-}
      - RESEARCH_LLM_ENDPOINT=${RESEARCH_LLM_ENDPOINT:-}
      - HF_TOKEN=${HF_TOKEN:-}
      - HUGGING_FACE_HUB_TOKEN=${HUGGING_FACE_HUB_TOKEN:-}
      - SEARXNG_INSTANCE=http://searxng:8080
      - CHROMADB_HOST=chromadb
      - CHROMADB_PORT=8000
      - DATABASE_URL=${DATABASE_URL:-sqlite:///./data/app.db}
      - AUTH_ENABLED=${AUTH_ENABLED:-true}
      - LOCALHOST_BYPASS=${LOCALHOST_BYPASS:-false}
      - COMPANION_BASE_URL=${COMPANION_BASE_URL:-}
      - ODYSSEUS_ADMIN_USER=${ODYSSEUS_ADMIN_USER:-admin}
      - ODYSSEUS_ADMIN_PASSWORD=${ODYSSEUS_ADMIN_PASSWORD:-}
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost,http://127.0.0.1}
      - SECURE_COOKIES=${SECURE_COOKIES:-}
      - EMBEDDING_URL=${EMBEDDING_URL:-}
      - EMBEDDING_MODEL=${EMBEDDING_MODEL:-}
      - EMBEDDING_API_KEY=${EMBEDDING_API_KEY:-}
      - FASTEMBED_MODEL=${FASTEMBED_MODEL:-sentence-transformers/all-MiniLM-L6-v2}
      - FASTEMBED_CACHE_PATH=${FASTEMBED_CACHE_PATH:-}
      - CLEANUP_INTERVAL_HOURS=${CLEANUP_INTERVAL_HOURS:-24}
      - ODYSSEUS_INPROCESS_POLLERS=${ODYSSEUS_INPROCESS_POLLERS:-1}
      - ODYSSEUS_INPROCESS_TASKS=${ODYSSEUS_INPROCESS_TASKS:-1}
      - ODYSSEUS_QWEN_NATIVE_COMPACT_BUILTINS=${ODYSSEUS_QWEN_NATIVE_COMPACT_BUILTINS:-1}
      - ODYSSEUS_QWEN_SUPPRESS_LOCAL_CONTEXT=${ODYSSEUS_QWEN_SUPPRESS_LOCAL_CONTEXT:-0}
      - ODYSSEUS_CAPTURE_MODEL_REQUESTS=${ODYSSEUS_CAPTURE_MODEL_REQUESTS:-0}
      - ODYSSEUS_MCP_EMAIL_OWNER=${ODYSSEUS_MCP_EMAIL_OWNER:-}
      - ODYSSEUS_SCRIPT_HOST=${ODYSSEUS_SCRIPT_HOST:-localhost}
      - ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=${ODYSSEUS_CHAT_UPLOAD_MAX_BYTES:-10485760}
      - ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES=${ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES:-104857600}
      - ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES=${ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES:-26214400}
      - ODYSSEUS_MEMORY_IMPORT_MAX_BYTES=${ODYSSEUS_MEMORY_IMPORT_MAX_BYTES:-10485760}
      - ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES=${ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES:-26214400}
      - ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=${ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES:-26214400}
      - ODYSSEUS_EDITOR_DRAFT_MAX_BYTES=${ODYSSEUS_EDITOR_DRAFT_MAX_BYTES:-268435456}
      - ODYSSEUS_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400}
      - ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760}
      - ODYSSEUS_TTS_CACHE_MAX_BYTES=${ODYSSEUS_TTS_CACHE_MAX_BYTES}
      # Host workspace translation is opt-in. Keep the public compose file
      # user-neutral; configure these in a local .env or use the host-workspace
      # overlay with ODYSSEUS_HOST_WORKSPACE_DIR.
      - ODYSSEUS_WORKSPACE_HOST_ROOT=${ODYSSEUS_WORKSPACE_HOST_ROOT:-}
      - ODYSSEUS_WORKSPACE_CONTAINER_ROOT=${ODYSSEUS_WORKSPACE_CONTAINER_ROOT:-/workspace}
      - ODYSSEUS_WORKSPACE_DEFAULT=${ODYSSEUS_WORKSPACE_DEFAULT:-}
      - DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
      - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
      - GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
      - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
      - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
      - GOOGLE_OAUTH_REDIRECT_URI=${GOOGLE_OAUTH_REDIRECT_URI:-}
      # Externally reachable origin for MCP OAuth callbacks. The container
      # always listens on 7000 and cannot see the host port map above, so
      # remote MCP OAuth needs this set whenever the browser reaches
      # Odysseus on anything other than http://localhost:7000.
      - OAUTH_REDIRECT_BASE_URL=${OAUTH_REDIRECT_BASE_URL:-}
      - TAVILY_API_KEY=${TAVILY_API_KEY:-}
      - SERPER_API_KEY=${SERPER_API_KEY:-}
      # PUID / PGID โ€” the user/group the container drops to before
      # running uvicorn (entrypoint also chowns /app/data + /app/logs
      # to match, so bind-mounted files stay editable from the host).
      # 1000 is the default first user on most Linux installs. If your
      # host user has a different id, override here or via .env, e.g.:
      #   PUID=1001
      #   PGID=1001
      # Find yours with:  id -u  /  id -g
      - PUID=${PUID:-1000}
      - PGID=${PGID:-1000}
    depends_on:
      searxng:
        condition: service_healthy
      chromadb:
        condition: service_started
    restart: unless-stopped
    # AMD ROCm overlay (from docker/gpu.amd.yml).
    devices:
      - /dev/kfd
      - /dev/dri
    group_add:
      - video
      - ${RENDER_GID:-render}

  chromadb:
    image: docker.io/chromadb/chroma:latest
    ports:
      - "${CHROMADB_BIND:-127.0.0.1}:8100:8000"
    volumes:
      # The image runs `chroma run /config.yaml`, whose persist_path is "/data".
      # It declares no VOLUME, so anything written outside a mount lives in the
      # container's writable layer and dies with it. /chroma/chroma is where
      # pre-1.0 images kept the store; mounting there now catches nothing.
      - chromadb-data:/data
    environment:
      - ANONYMIZED_TELEMETRY=FALSE
    restart: unless-stopped

  searxng:
    # Pinned, not :latest โ€” odysseus waits on searxng's healthcheck
    # (depends_on: condition: service_healthy), so a broken upstream `latest`
    # tag blocks the whole app from starting. 2026.6.2 crashes on boot with
    # `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414).
    # Bump this deliberately after verifying a newer tag boots clean.
    image: docker.io/searxng/searxng:2026.9.25-12f8b6515@sha256:5286edb35782454ab8a102c5eff6b54bff745853191b46aeead95f225aa6dfb6
    entrypoint:
      - /bin/sh
      - -c
      - |
        set -eu
        if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then
          secret="$${SEARXNG_SECRET:-}"
          if [ -z "$$secret" ]; then
            secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
          fi
          sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml
        fi
        # Advisory: a settings file the migration cannot parse or rewrite must
        # not be what stops searxng from booting. It explains itself on stderr
        # and we carry on, letting searxng report anything genuinely wrong.
        /usr/local/searxng/.venv/bin/python /tmp/migrate-searxng-settings.py /etc/searxng/settings.yml || true
        exec /usr/local/searxng/entrypoint.sh
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - searxng-data:/etc/searxng
      - ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z
      - ./scripts/migrate_searxng_settings.py:/tmp/migrate-searxng-settings.py:ro,z
    environment:
      - SEARXNG_BASE_URL=http://localhost:8080/
      - SEARXNG_SECRET=${SEARXNG_SECRET:-}
    # The official searxng image runs as the non-root `searxng` user, but its
    # entrypoint still needs to chown /etc/searxng on first boot, drop privs via
    # su-exec, and (with our wrapper above) write settings.yml into the named
    # volume. Without these capabilities the wrapper aborts at the redirection
    # with EACCES and the container fails its healthcheck with permission
    # errors during setup. Mirrors the cap set recommended by the upstream
    # searxng-docker compose file. See issue #721.
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
      - DAC_OVERRIDE
    healthcheck:
      test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""]
      interval: 5s
      timeout: 6s
      retries: 20
      start_period: 10s
    restart: unless-stopped

  ntfy:
    image: docker.io/binwiederhier/ntfy
    command: serve
    ports:
      - "${NTFY_BIND:-127.0.0.1}:8091:80"
    volumes:
      - ntfy-cache:/var/cache/ntfy
    environment:
      - NTFY_BASE_URL=${NTFY_BASE_URL:-http://localhost:8091}
    restart: unless-stopped

volumes:
  searxng-data:
  chromadb-data:
  ntfy-cache: