# ───────────────────────────────────────────────────────────────────────────── # 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: