Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions docker-compose.coolify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Coolify-ready deployment. The regular docker-compose.yml keeps working
# everywhere else; use this file only when deploying with Coolify.
#
# Why a separate file instead of changing docker-compose.yml:
# - Coolify's compose parser rewrites bind-mount sources and chokes on
# `${APP_DATA_DIR:-./data}/ssh`-style interpolation ("invalid spec:
# ... empty section between colons"). This file uses named volumes
# only, so there is nothing to mangle and data survives redeploys.
# - It runs the pre-built ghcr.io image instead of building on the
# server. A full multi-stage build takes 10+ minutes and a lot of
# RAM on a small VPS; a pull takes seconds.
# - No host port bindings. Coolify's Traefik proxy reaches services
# over the compose network (the SERVICE_FQDN_ODYSSEUS_7000 line
# tells it to route the domain to port 7000), and dropping the
# bindings avoids host port collisions (e.g. searxng's 8080).
#
# Step-by-step guide: docs/deploy/coolify.md

services:
odysseus:
image: ghcr.io/pewdiepie-archdaemon/odysseus:latest
volumes:
- odysseus-data:/app/data
- odysseus-logs:/app/logs
# Cookbook remote-server SSH identity. Odysseus can generate a key here;
# add the shown public key to each remote server's authorized_keys.
- odysseus-ssh:/app/.ssh
# Cookbook local model cache (HuggingFace).
- odysseus-huggingface:/app/.cache/huggingface
# Cookbook-installed Python CLIs/packages (vLLM, llama-cpp-python, etc.).
# Persist them so a container recreate does not remove installed engines.
- odysseus-local:/app/.local
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:
# Coolify magic variable: generates a domain for this service and
# routes it to container port 7000. Harmless outside Coolify.
- SERVICE_FQDN_ODYSSEUS_7000
- 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}
- ODYSSEUS_ADMIN_USER=${ODYSSEUS_ADMIN_USER:-admin}
# Coolify magic variable: a random password is generated on first
# deploy and shown in the service's Environment Variables tab.
- ODYSSEUS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
# The public URL Coolify assigned to this service (scheme included),
# so browser requests from your domain pass the CORS check.
- ALLOWED_ORIGINS=${SERVICE_URL_ODYSSEUS}
# Coolify terminates TLS at its proxy, so cookies can be Secure.
- SECURE_COOKIES=${SECURE_COOKIES:-true}
- 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_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_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400}
- ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760}
- DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
- GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
- TAVILY_API_KEY=${TAVILY_API_KEY:-}
- SERPER_API_KEY=${SERPER_API_KEY:-}
# With named volumes there is no host user to match; the entrypoint
# still drops privileges to this UID/GID inside the container.
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:7000/ >/dev/null"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
depends_on:
searxng:
condition: service_healthy
chromadb:
condition: service_started
restart: unless-stopped

chromadb:
image: docker.io/chromadb/chroma:latest
volumes:
- chromadb-data:/chroma/chroma
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. Keep this tag in sync with
# docker-compose.yml.
image: docker.io/searxng/searxng:2026.5.31-7159b8aed
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
exec /usr/local/searxng/entrypoint.sh
volumes:
- searxng-data:/etc/searxng
- ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro
environment:
- SEARXNG_BASE_URL=http://localhost:8080/
- SEARXNG_SECRET=${SEARXNG_SECRET:-}
# Same capability set as docker-compose.yml: the official image's
# entrypoint needs to chown /etc/searxng and drop privileges on first
# boot; without these it fails its healthcheck with EACCES.
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
volumes:
- ntfy-cache:/var/cache/ntfy
environment:
# Only needed if you attach a public domain to this service in
# Coolify; set it to that URL so notification links resolve.
- NTFY_BASE_URL=${NTFY_BASE_URL:-}
restart: unless-stopped

volumes:
odysseus-data:
odysseus-logs:
odysseus-ssh:
odysseus-huggingface:
odysseus-local:
searxng-data:
chromadb-data:
ntfy-cache:
41 changes: 41 additions & 0 deletions docs/deploy/coolify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Deploying Odysseus on Coolify

Odysseus ships a Coolify-ready compose file, [`docker-compose.coolify.yml`](../../docker-compose.coolify.yml). It runs the pre-built image from GHCR (no 10-minute build on your VPS), uses named volumes only (Coolify's compose parser breaks `${VAR:-./data}/subdir`-style bind mounts), and publishes no host ports (Coolify's Traefik proxy routes to the container directly, and nothing collides with other stacks on the same server).

The regular `docker-compose.yml` is unchanged and remains the way to run Odysseus everywhere else.

## Requirements

- Coolify v4 on a server with ~2 GB free RAM and ~10 GB free disk (the image plus ChromaDB, SearXNG, and ntfy).
- A domain pointed at the server, or use the `sslip.io` domain Coolify generates for you.

## Steps

1. In Coolify: **+ New** → **Public Repository**.
2. Repository URL: `https://github.com/pewdiepie-archdaemon/odysseus`, branch `main`.
3. Set **Build Pack** to **Docker Compose** and **Docker Compose Location** to `/docker-compose.coolify.yml`, then continue.
4. Coolify lists the four services. On the `odysseus` service, set your domain (or keep the generated one). The compose file already tells the proxy to route it to port 7000 via the `SERVICE_FQDN_ODYSSEUS_7000` magic variable — you don't need to configure a port.
5. Click **Deploy**. The first deploy pulls the image (a few minutes); later deploys are fast.

## First login

- Username: `admin` (or whatever you set `ODYSSEUS_ADMIN_USER` to).
- Password: Coolify generates one on first deploy — find `SERVICE_PASSWORD_ADMIN` in the resource's **Environment Variables** tab. Change it in **Settings** after logging in.

## Notes

- **Updating:** click **Redeploy**. It re-pulls `ghcr.io/pewdiepie-archdaemon/odysseus:latest` (curated releases). For the rolling development build, change the image tag to `:dev` in the compose file or via Coolify's compose editor.
- **Persistence:** all state lives in named volumes (`odysseus-data`, `odysseus-logs`, SSH keys, the HuggingFace cache, and Cookbook-installed engines), so redeploys and image updates keep your data.
- **CORS:** `ALLOWED_ORIGINS` is set automatically from your service URL. If logins fail with CORS errors after changing the domain, check that variable matches `https://your-domain`.
- **Ollama on the same server:** the container reaches the host at `http://host.docker.internal:11434`. If Ollama runs as a separate Coolify service instead, use its service URL.
- **ntfy notifications:** the bundled ntfy service is internal-only by default. To use it from outside (e.g. the mobile app), attach a domain to the `ntfy` service in Coolify and set `NTFY_BASE_URL` to that URL.

## Why not the regular docker-compose.yml?

If you point Coolify at `docker-compose.yml` you will hit three problems:

1. Coolify rewrites bind-mount sources and mangles `${APP_DATA_DIR:-./data}/ssh`-style entries into empty strings, failing with `invalid spec: :/app/.ssh:z: empty section between colons`.
2. It builds the image from source on your server — a multi-stage build that takes 10+ minutes and can OOM small VPSes.
3. Ports are bound to `127.0.0.1` on the host and SearXNG claims host port 8080, which collides with Coolify's own proxy setups.

`docker-compose.coolify.yml` avoids all three.
3 changes: 3 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ binds the web UI to `127.0.0.1` by default. If the port is taken, set
`APP_PORT=7001` in `.env` and recreate the container. Set `APP_BIND=0.0.0.0`
only when you intentionally want LAN/reverse-proxy access.

Deploying with [Coolify](https://coolify.io)? Use `docker-compose.coolify.yml`
instead — see [docs/deploy/coolify.md](deploy/coolify.md).

> **On Apple Silicon (M-series) Macs:** Docker can't reach the Metal GPU, so
> Cookbook serves local models on CPU only. For GPU-accelerated model serving,
> run natively instead — see [Apple Silicon](#apple-silicon) below.
Expand Down
Loading