docker-compose.ollama.yml
Rama main · réplica de lectura del repositorio original.
# ─────────────────────────────────────────────────────────────────────────────
# Optional: bundle a local Ollama so the stack runs FULLY OFFLINE — no OpenAI
# key, no Ollama on the host. Layer it on top of the quickstart:
#
# docker compose -f docker-compose.quickstart.yml -f docker-compose.ollama.yml up -d
#
# install.sh adds this overlay for you when you pick the internal LLM, and
# writes the same `-f` set to ~/rsync-ai/compose.sh so a later `up`/`down`/`logs`
# carries it too. Combine freely with the byo-postgres and byo-kafka overlays;
# all three are independent.
#
# Set LLM_PROVIDER=ollama in .env. Without this overlay that setting points the
# LLM tier at an Ollama running on the HOST (http://host.docker.internal:11434),
# which is the right answer when you already run one and the wrong one when you
# do not — nothing starts an Ollama on your behalf. This file starts one.
#
# ── The model is pulled for you ──────────────────────────────────────────────
# `ollama-pull` below downloads the model once, into a named volume, before any
# service that would ask for it starts. The first `up` therefore BLOCKS for as
# long as that download takes — several minutes on a home connection, and about
# 4.7 GB on disk for the default qwen2.5:7b. Watch it with:
#
# docker logs -f rsync-ai-ollama-pull
#
# Later `up`s are a no-op: the volume survives `down`, and the pull step exits
# immediately when the model is already there. `docker compose down -v` deletes
# the volume and the next `up` downloads again.
#
# ── Memory ───────────────────────────────────────────────────────────────────
# A 7B model at 4-bit wants ~5 GB resident on top of everything else the stack
# runs, so plan for 12 GB of RAM. install.sh checks this before it starts the
# stack; a hand-run `docker compose up` does not, and the failure mode is the
# model loading, the host swapping, and inference taking minutes per token.
# ─────────────────────────────────────────────────────────────────────────────
# The model name, resolved ONCE and referenced everywhere below.
#
# The precedence is not a house choice — it mirrors get_default_model() in
# llm-service/src/utils/openai_client.py:120-138, which is the function the
# running code calls to decide what to ask Ollama for: LLM_MODEL first, then
# OLLAMA_MODEL, then qwen2.5:7b. Pulling a model by any other rule pulls a model
# the stack will not request, and the operator gets a 4.7 GB download followed
# by `model not found` on the first prompt.
x-ollama-model: &ollama-model ${LLM_MODEL:-${OLLAMA_MODEL:-qwen2.5:7b}}
services:
ollama:
# Pinned: `latest` is what the README's no-OpenAI-key path pulls, and a model runtime
# that changes under a re-pull changes inference behaviour, not just the binary.
image: ollama/ollama:0.33.2
container_name: rsync-ai-ollama
restart: unless-stopped
logging:
driver: json-file
options:
max-size: ${RSYNC_LOG_MAX_SIZE:-10m}
max-file: "${RSYNC_LOG_MAX_FILE:-3}"
# Deliberately NO mem_limit. Every other service here gets one because its
# working set is bounded and a leak should be capped; this one's working set
# IS the model, and a cap below it does not slow inference down, it gets the
# container OOM-killed mid-answer. Size the HOST instead — see the note above.
volumes:
- ollama_models:/root/.ollama
healthcheck:
# `ollama list` is a round trip to the server's own API, so it goes green
# only once the HTTP listener is actually accepting — which is what the
# pull step below is waiting for. A port check would pass earlier and
# hand the pull a connection the server is not ready to serve.
test: ["CMD", "ollama", "list"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# For GPU acceleration on a CUDA host, uncomment:
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
# ─── MODEL BOOTSTRAP ────────────────────────────────────────────────────────
#
# Until this service existed, starting the bundled Ollama gave you a server
# with no models in it, and the only thing telling you to fix that was a line
# in this file's header:
#
# docker exec rsync-ai-ollama ollama pull qwen2.5:7b
#
# Skipping it does not fail loudly. Ollama answers the request and returns
# `model "qwen2.5:7b" not found, try pulling it first`; the Python tier
# surfaces that as a failed generation, and the stack around it stays green —
# every container `running`, `/ready` 200, nothing to point at. The whole
# offline path was one undocumented-by-anything-executable step away from
# working, which is the same defect class as the manual CREATE DATABASE this
# release removed from the Postgres paths.
#
# Mirrors the chart's templates/infra/ollama.yaml, which runs the same pull as
# a post-install hook Job. The two are hand-kept in step for the same reason
# kafka-init's script is inline: install.sh downloads compose files alone,
# with no repo checkout, so there is no shared script on the host to call.
ollama-pull:
image: ollama/ollama:0.33.2
container_name: rsync-ai-ollama-pull
restart: "no"
logging:
driver: json-file
options:
max-size: ${RSYNC_LOG_MAX_SIZE:-10m}
max-file: "${RSYNC_LOG_MAX_FILE:-3}"
# Run-once job, and a thin one: it holds no model bytes at all. Both
# commands below are HTTP calls to the `ollama` container, which does the
# downloading into its own volume — which is also why this service mounts
# nothing. A second writer on ollama_models would race the server.
mem_limit: 512m
depends_on:
ollama:
condition: service_healthy
environment:
# The CLI is a client. Without this it looks for a server on localhost
# inside its own container, finds nothing, and reports a connection error
# that reads like the server is down when it is up and healthy next door.
OLLAMA_HOST: http://ollama:11434
OLLAMA_MODEL: *ollama-model
entrypoint: ["/bin/sh", "-c"]
# Single-element list, not a bare block scalar: Compose splits a string
# command into argv, which would leave `sh -c` running only the first word.
# `docker compose config` must show ONE item.
command:
- |
set -u;
echo "model: $$OLLAMA_MODEL";
if ollama show "$$OLLAMA_MODEL" >/dev/null 2>&1; then
echo "already on the server; nothing to download";
exit 0;
fi;
echo "downloading $$OLLAMA_MODEL -- this is a few GB and only happens once";
# The error is captured rather than discarded because the failures that
# land here read identically from outside: a model name with a typo, a
# registry that is down, and a disk with no room all just end this loop.
# The operator needs Ollama's own words to tell them apart.
ERR="";
n=0;
until ERR="$$(ollama pull "$$OLLAMA_MODEL" 2>&1)"; do
n=$$((n + 1));
if [ "$$n" -ge 5 ]; then
echo "FATAL: could not pull \"$$OLLAMA_MODEL\" after 5 attempts." >&2;
echo " Last error from ollama: $$ERR" >&2;
echo " Checked in this order:" >&2;
echo " * the name is a real tag at https://ollama.com/library" >&2;
echo " (LLM_MODEL in .env, or OLLAMA_MODEL if LLM_MODEL is unset)" >&2;
echo " * this host can reach registry.ollama.ai over HTTPS" >&2;
echo " * the docker volume has room for the model (several GB)" >&2;
echo " Nothing downstream starts until this succeeds, by design: a" >&2;
echo " server with no model answers every prompt with 'model not found'." >&2;
exit 1;
fi;
echo "pull attempt $$n/5 failed, retrying in 10s: $$ERR" >&2;
sleep 10;
done;
echo "$$OLLAMA_MODEL is ready";
llm-service:
environment:
OLLAMA_URL: http://ollama:11434
# OLLAMA_BASE_URL as well, not OLLAMA_URL alone. _ollama_base_url in
# llm-service/src/utils/openai_client.py:70 reads BASE_URL first and only
# falls back to URL when BASE_URL is unset or empty -- and
# docker-compose.yml:1585 hard-codes
# OLLAMA_BASE_URL=http://host.docker.internal:11434/v1 on tool-generator,
# a line docker-compose.prod.yml:789 repeats. With only OLLAMA_URL set
# here, that service kept dialling the host on both of those bases while
# this overlay's whole promise is that it does not have to. Rendered
# `docker compose -f docker-compose.yml -f docker-compose.ollama.yml config`
# and read the key back to confirm. The trailing /v1 is what the helper
# appends anyway; writing it keeps the rendered value byte-identical in
# shape to the ones the base files already carry.
OLLAMA_BASE_URL: http://ollama:11434/v1
# Pinned. The quickstart's default for this key used to be
# `${LLM_MODEL:-gpt-4o}`, so a hand-run
# `docker compose -f docker-compose.quickstart.yml -f docker-compose.ollama.yml up -d`
# with nothing set handed this container LLM_MODEL=gpt-4o while ollama-pull
# downloaded qwen2.5:7b -- and get_default_model returns LLM_MODEL before it
# ever looks at OLLAMA_MODEL, so the main chat path asked the bundled Ollama
# for an OpenAI catalog name. That default is empty now, but an .env kept
# from an OpenAI install still says LLM_MODEL=gpt-4o, and the pin covers it.
LLM_MODEL: *ollama-model
# Not redundant with LLM_MODEL above, and not decoration. FOUR functions in
# llm-service/src/utils/openai_client.py pick a model name for the Ollama
# provider, and left alone they pick THREE different ones — while
# ollama-pull downloads exactly one:
#
# get_default_model :120-138 LLM_MODEL, OLLAMA_MODEL, qwen2.5:7b
# explorer_default_model :164-194 OLLAMA_MODEL, llama3:latest
# explorer_default_sql_model :197-206 OLLAMA_MODEL, sqlcoder:latest
# rank_tables_default_model :209-235 RANK_TABLES_MODEL, OLLAMA_MODEL, llama3:latest
#
# So without these two lines the main chat path works on the pulled model
# while the Data Explorer and the table-ranking step ask Ollama for
# `llama3:latest` and `sqlcoder:latest`, which are not in the volume and
# are not going to be. The operator sees a working /chat beside a Data
# Explorer that fails — the exact split-brain this overlay exists to end.
OLLAMA_MODEL: *ollama-model
# rank-tables is the one that reads a variable of its own BEFORE
# OLLAMA_MODEL, deliberately: on the cloud providers it is pinned to a
# cheap model so a stack-wide upgrade cannot silently multiply its bill.
# Offline it does fall through to OLLAMA_MODEL (:227-228), so the line
# below is belt and braces rather than the only thing holding it -- keep
# it, because it is also what an operator greps for when they want this
# one endpoint moved somewhere else.
RANK_TABLES_MODEL: *ollama-model
# A fifth model name, and the only one that is a LIST. When the Explorer's
# NL->SQL step returns something that is not a SELECT, it retries against
# each name in EXPLORER_SQL_FALLBACK_MODELS in turn
# (llm-service/src/gateway/main.py:2756-2782), and its built-in default is
# the literal `qwen2.5:7b,llama3:latest,codellama:7b-instruct` -- names
# from the era when this file pulled three models. ollama-pull downloads
# one, so on a default install two of those three are guaranteed 404s
# against the bundled server, spent inside the Explorer's request budget
# before the same error is raised anyway; set LLM_MODEL to anything else
# and all three are wrong. Pinning it to the pulled model makes the list
# collapse to empty -- the loop filters out any name equal to the primary
# -- which is the honest answer on a one-model install: there is no other
# model to fall back to, so fail immediately instead of slowly.
EXPLORER_SQL_FALLBACK_MODELS: *ollama-model
depends_on:
# service_completed_successfully, and deliberately WITHOUT `required:
# false`. That flag reads like the house style in the byo-* overlays, and
# here it would quietly undo this whole file: measured on Compose 2.x, a
# FAILED completion dependency marked `required: false` degrades to the
# warning `optional dependency "…" didn't complete successfully` and the
# dependent starts anyway with `up` still exiting 0 — which is exactly the
# start-on-an-empty-Ollama this service exists to prevent. The flag is for
# a service that may be absent from the project; ollama-pull is defined
# right here and never is.
ollama-pull:
condition: service_completed_successfully
tool-generator:
environment:
OLLAMA_URL: http://ollama:11434
# This is the service the base files pin OLLAMA_BASE_URL on, so without
# the line below the overlay's OLLAMA_URL never wins here. See llm-service.
OLLAMA_BASE_URL: http://ollama:11434/v1
# Both keys, for the reason spelled out under llm-service: whichever of the
# two a service happens to read, it reads the model that is on disk.
LLM_MODEL: *ollama-model
OLLAMA_MODEL: *ollama-model
depends_on:
ollama-pull:
condition: service_completed_successfully
planner:
environment:
OLLAMA_URL: http://ollama:11434
# No base file pins OLLAMA_BASE_URL on planner today. Set anyway, so the
# three extended services resolve the same way and a later base-file edit
# cannot strand this one the way it stranded tool-generator.
OLLAMA_BASE_URL: http://ollama:11434/v1
# Takes LLM_MODEL from the same .env as llm-service, so it needs the same
# pin llm-service does.
LLM_MODEL: *ollama-model
OLLAMA_MODEL: *ollama-model
depends_on:
ollama-pull:
condition: service_completed_successfully
volumes:
ollama_models: