Files
LLM-Server/docker-compose.yml
T
haylanandClaude-Bot 977e9d3dd7 fix(omniroute): healthcheck used python3, which the image doesn't have
Confirmed live on the R9700: omniroute starts up fine ("[API Bridge]
Listening on 0.0.0.0:20129") but docker reported it unhealthy forever -
the healthcheck's python3 -c "..." command can never run (which python3
wget curl node found only node in the image), so it failed every single
check regardless of actual app health.

Switched to a node-based TCP-connect check on the same port instead of an
HTTP GET against /healthz - OmniRoute's own Docker guide already treats a
bare TCP probe as an acceptable liveness check, and this sidesteps needing
to confirm /healthz's exact path/response shape on this image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPZ6TogJiYxG8E4EQBB197
2026-09-03 20:08:58 +02:00

134 lines
5.5 KiB
YAML

services:
llama-server:
image: ghcr.io/ggml-org/llama.cpp:server-rocm
container_name: llama-server
devices:
- /dev/kfd
- /dev/dri
group_add:
- video
- render
security_opt:
- seccomp=unconfined
ipc: host
volumes:
- models:/models
command: >
-m /models/${LLAMA_MODEL_FILE:-Qwen3.8-27B-UD-Q4_K_XL.gguf}
--host 0.0.0.0
--port 8080
--n-gpu-layers ${LLAMA_GPU_LAYERS:-999}
--ctx-size ${LLAMA_CTX_SIZE:-131072}
--jinja
# No published host port: llama-server is reached only via the omniroute
# gateway on the ai-stack docker network now — see issue #15. Its
# unauthenticated API no longer needs to be LAN-reachable directly.
expose:
- "8080"
restart: unless-stopped
networks: [ai-stack]
labels:
# ponytail: idle-timeout tuning lives here, not in a separate lazytainer config file —
# one place to look. Raise LAZYTAINER_INACTIVE_TIMEOUT if 15 min proves too eager.
- "lazytainer.group.llamaserver.sleepMethod=stop"
- "lazytainer.group.llamaserver.ports=8080"
- "lazytainer.group.llamaserver.inactiveTimeout=${LAZYTAINER_INACTIVE_TIMEOUT:-900}"
- "lazytainer.group.llamaserver.minPacketThreshold=2"
# ponytail: one-off downloader, not a standing service — run via
# `docker compose --profile tools run --rm downloader`. Folded into
# scripts/update.sh, which runs this every time; the `test -f` guard is
# what makes that safe to re-run without re-downloading. Keeps the model
# file inside the named `models` volume instead of a host bind-mount.
downloader:
image: curlimages/curl:latest
profiles: ["tools"]
# ponytail: named volume is created root-owned; curl_user (uid 100) can't
# write into it otherwise, so run as root for this one-off job.
user: root
volumes:
- models:/models
entrypoint: ["sh", "-c"]
command:
- >
test -f /models/${LLAMA_MODEL_FILE:-Qwen3.8-27B-UD-Q4_K_XL.gguf} &&
echo "already downloaded, skipping" ||
curl -L --fail --create-dirs -o /models/${LLAMA_MODEL_FILE:-Qwen3.8-27B-UD-Q4_K_XL.gguf}
https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/main/${LLAMA_MODEL_FILE:-Qwen3.8-27B-UD-Q4_K_XL.gguf}
# Replaces litellm — see issue #31 (wayfinder map) for the full migration
# rationale/findings. No static config.yaml equivalent: provider routing
# (llama-server, searxng-search) is registered once through the dashboard
# or POST /api/providers after first boot, not checked into this repo —
# see docs/proxy-key-onboarding.md.
omniroute:
image: diegosouzapw/omniroute:latest
container_name: omniroute
depends_on:
llama-server:
condition: service_started
volumes:
- omniroute-data:/app/data
env_file: .env
environment:
# Split-port mode: dashboard and API are fully separate ports (unlike
# LiteLLM's single :4000 for both /v1 and /ui) — only API_PORT is
# published below, so the dashboard has no network route in from
# outside this container at all. No NPM path-deny rule needed.
- API_HOST=0.0.0.0
- API_PORT=${OMNIROUTE_API_PORT:-20129}
- DASHBOARD_PORT=${OMNIROUTE_DASHBOARD_PORT:-20128}
# Required to register llama-server/searxng-search as providers —
# their base URLs are LAN/container-internal addresses, blocked by
# default (SSRF guard against public-provider spoofing).
- OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true
- OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS=true
# Same reasoning as litellm's extra_hosts entry below — ai-stack's bridge
# network can't resolve search.home on its own.
extra_hosts:
- "search.home:${SEARXNG_LAN_IP}"
ports:
- "${OMNIROUTE_API_PORT:-20129}:${OMNIROUTE_API_PORT:-20129}"
restart: unless-stopped
networks: [ai-stack]
# ponytail: TCP-connect check, not an HTTP /healthz GET — the image has
# no python3/curl/wget (confirmed live, `which` found only node), and
# OmniRoute's own Docker guide already treats a bare TCP probe on this
# port as an acceptable liveness check, not just the HTTP one. Simpler
# and avoids depending on /healthz's exact path/response shape.
healthcheck:
test:
- CMD-SHELL
- node -e "require('net').connect(${OMNIROUTE_API_PORT:-20129},'localhost').on('connect',function(){this.end();process.exit(0)}).on('error',()=>process.exit(1))"
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
lazytainer:
image: ghcr.io/vmorganp/lazytainer:master
container_name: lazytainer
# NOT network_mode: host — lazytainer identifies its own container by
# matching os.Hostname() against the Docker container-ID list
# (vmorganp/Lazytainer, configureFromLabels()); under host networking the
# container inherits the host's hostname instead of its own ID, so that
# match always fails and it panics with "Could not determine container ID
# of lazytainer" on every start. Host networking also can't see traffic
# to llama-server:8080 anyway — that port only exists on the ai-stack
# bridge network (no host port published, see issue #15 above). Joining
# ai-stack instead fixes both: hostname becomes the real container ID,
# and it's on the same network as the traffic it's watching.
networks: [ai-stack]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
depends_on:
- llama-server
networks:
ai-stack:
volumes:
models:
omniroute-data: