{"id":1159,"date":"2026-10-29T10:00:00","date_gmt":"2026-10-28T23:00:00","guid":{"rendered":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/"},"modified":"2026-11-03T20:28:19","modified_gmt":"2026-11-03T09:28:19","slug":"local-ai-litellm-setup-guide","status":"publish","type":"post","link":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/","title":{"rendered":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide"},"content":{"rendered":"\n<p class=\"has-pale-cyan-blue-background-color has-background wp-block-paragraph\">A step-by-step guide to running AI on your own Linux server: Ollama kept private, LiteLLM as the only way in, a key per app, a chat UI, tracing and a locked-down server. Every step ends with a check.<\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Where this fits \u00b7 Running Local AI in Production \u00b7 Quick-start guide<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:17px\">This is the hands-on companion to Running Local AI in Production, an 11-part series where we built a private AI server in our lab and ran realistic test workloads on it for seven weeks. Follow it step by step; each step links to the part that explains the why in depth.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><a href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a> &nbsp;\u00b7&nbsp; <a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 1: Local AI or Cloud AI? Why We Built a Private AI Server \u2192<\/a><\/p>\n\n\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See all 11 parts of the series<\/summary>\n\n<ol class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Local AI or Cloud AI? Why We Built a Private AI Server<\/a>: why we built the lab on our own hardware, what ran on it and what that choice cost<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Control Access to Local AI Models? Use a Gateway<\/a>: one LLM gateway as the only way to reach a model: keys, budgets and tracing<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Does a Local AI Server Freeze? Sizing Memory for LLMs<\/a>: five hard freezes and the memory budget that ended them<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Happens When a Prompt Is Too Big for a Local LLM?<\/a>: fitting every prompt to a local model&#8217;s context window<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/smart-routing-single-gpu-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Run Several AI Models on One GPU Without Slowdowns?<\/a>: routing requests across models on a single GPU without constant model swaps<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Keep an AI Server Stable? GitOps for Local AI<\/a>: running the server from a Git repository: deploys, drift, pinning and safe AI agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Is Your Local AI Server Actually Doing? Observability Basics<\/a>: making the server observable: traces, dashboards, usage and heat<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/multi-agent-stock-analysis-local-llm\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can Local AI Agents Analyse Stocks? How We Tested and Scored Them<\/a>: a multi-agent stock-analysis experiment and how we scored the agents<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/telegram-ai-agent-local-llms\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Was Our Telegram AI Agent So Slow? The Hidden Prompt Cost<\/a>: a Telegram AI agent on local models, and why its fixed prompt became the budget<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-business-workflows\/\" target=\"_blank\" rel=\"noreferrer noopener\">Can AI Automate Business Workflows Without Sending Data Out?<\/a>: document, email and knowledge-base workflows that keep data on the server<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/private-ai-lab-lessons-and-whats-next\/\" target=\"_blank\" rel=\"noreferrer noopener\">Is Private AI Worth It? Lessons From Seven Weeks in Our Lab<\/a>: the lessons from seven weeks, the honest limits and what comes next<\/li>\n\n<\/ol>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:16px\"><em>This series documents a private AI server we built and stress-tested in our lab. Workloads and data are synthetic or generalised; the problems, measurements and fixes are real.<\/em><\/p>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">TL;DR<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Run models with Ollama on the server&#8217;s loopback address, put LiteLLM in front as the only way in, and give every app its own key. Allow most of a day.<\/li>\n\n\n<li>Size memory by measurement. Each parallel request slot reserves the full context window, and that reserved memory, not the weights, is what froze our lab server.<\/li>\n\n\n<li>Refuse oversized prompts at the gateway: Ollama otherwise cuts them silently, and LiteLLM&#8217;s <code>max_input_tokens<\/code> alone does nothing.<\/li>\n\n\n<li>Lock it down: loopback everywhere, TLS for anything remote, no silent local-to-cloud fallback, no secrets in readable files.<\/li>\n\n\n<li>Run it like production: pinned versions, config in Git, soak-tested upgrades, tested backups and an outside heartbeat.<\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Jargon buster: 15 terms used in this post, explained in plain English<\/summary>\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Context window<\/strong>: The maximum number of tokens a model can consider at once, covering both the prompt and its answer.<\/li>\n\n\n<li><strong>Slot (parallel requests)<\/strong>: One request a model server can work on at the same time. More slots serve more people at once but reserve more memory.<\/li>\n\n\n<li><strong>Ollama<\/strong>: Free, open-source software that downloads and runs AI models on your own machine and answers requests over a local API.<\/li>\n\n\n<li><strong>LiteLLM<\/strong>: Open-source software that puts one front door in front of many AI models, with per-user keys, budgets, limits and logging.<\/li>\n\n\n<li><strong>LLM gateway<\/strong>: A single service every AI request must pass through, so access, budgets and logging are controlled in one place.<\/li>\n\n\n<li><strong>Rate limit<\/strong>: A cap on how many requests a key can make in a given time, so one program can&#8217;t swamp the server.<\/li>\n\n\n<li><strong>Open WebUI<\/strong>: An open-source, browser-based chat interface, similar to ChatGPT, that can talk to models running on your own server.<\/li>\n\n\n<li><strong>Langfuse<\/strong>: Open-source software that records every AI request (prompt, answer, timing and cost) so you can see what happened and why.<\/li>\n\n\n<li><strong>Trace<\/strong>: A detailed record of one request as it passes through each part of a system, used to find where time or errors come from.<\/li>\n\n\n<li><strong>Docker \/ container<\/strong>: A way of packaging an application with everything it needs so it runs the same anywhere; each packaged application runs in its own container.<\/li>\n\n\n<li><strong>Docker Compose<\/strong>: A tool that defines and starts a set of related containers from one configuration file.<\/li>\n\n\n<li><strong>Pinning<\/strong>: Locking software to an exact version so it can&#8217;t change unexpectedly during an update.<\/li>\n\n\n<li><strong>Loopback<\/strong>: A network address that only the server itself can reach, so a service listening there isn&#8217;t exposed to the network.<\/li>\n\n\n<li><strong>systemd<\/strong>: The service manager on most Linux systems: it starts, stops and supervises background services.<\/li>\n\n\n<li><strong>PostgreSQL \/ ClickHouse \/ Redis<\/strong>: Open-source databases: PostgreSQL for general records, ClickHouse for fast analysis of large logs, Redis for fast short-term data.<\/li>\n\n<\/ul>\n\n<\/details>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h3 class=\"wp-block-heading\">In this post<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"#what-you-will-end-up-with\">What you will end up with<\/a><\/li>\n\n\n<li><a href=\"#before-you-start\">Before you start<\/a><\/li>\n\n\n<li><a href=\"#step-1-install-ollama-and-keep-it-private\">Step 1: Install Ollama and keep it private<\/a><\/li>\n\n\n<li><a href=\"#step-2-choose-models-and-measure-them\">Step 2: Choose models and measure them<\/a><\/li>\n\n\n<li><a href=\"#step-3-put-litellm-in-front-as-the-only-door\">Step 3: Put LiteLLM in front as the only door<\/a><\/li>\n\n\n<li><a href=\"#step-4-give-every-app-its-own-key\">Step 4: Give every app its own key<\/a><\/li>\n\n\n<li><a href=\"#step-5-stop-oversized-prompts-before-they-reach-the-gpu\">Step 5: Stop oversized prompts before they reach the GPU<\/a><\/li>\n\n\n<li><a href=\"#step-6-connect-a-chat-ui-and-your-first-app\">Step 6: Connect a chat UI and your first app<\/a><\/li>\n\n\n<li><a href=\"#step-7-see-what-is-happening\">Step 7: See what is happening<\/a><\/li>\n\n\n<li><a href=\"#step-8-lock-it-down\">Step 8: Lock it down<\/a><\/li>\n\n\n<li><a href=\"#step-9-keep-it-running\">Step 9: Keep it running<\/a><\/li>\n\n\n<li><a href=\"#best-practice-checklist\">Best-practice checklist<\/a><\/li>\n\n\n<li><a href=\"#troubleshooting\">Troubleshooting<\/a><\/li>\n\n\n<li><a href=\"#where-to-go-deeper\">Where to go deeper<\/a><\/li>\n\n<\/ul>\n\n<\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The practical companion to our 11-part series: what to build, in what order, and how to prove it works.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"what-you-will-end-up-with\">What you will end up with<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At the top are your apps, scripts, coding assistants and a chat UI (Open WebUI). All of them talk to one OpenAI-compatible address, the <strong>LiteLLM gateway<\/strong>, which checks each caller&#8217;s key, models, rate limits and budget, refuses anything too big, and logs and traces every call. Behind it sits <strong>Ollama<\/strong>, the model server, listening only on the server&#8217;s loopback address (<code>127.0.0.1<\/code>), running the models on the <strong>GPU<\/strong>. Nothing leaves the server unless you deliberately allow it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is for IT teams and developers comfortable on a Linux command line. Local AI fits sensitive data, a fixed cost and control over when a model changes; the cloud still fits public data, bursty workloads and the largest models. Many teams use both, kept apart by rules the gateway enforces.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"before-you-start\">Before you start<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hardware.<\/strong> What matters is memory the GPU can use: VRAM, or the shared pool on a unified-memory machine. Rough figures for 4-bit models with one request slot; measure your own in Step 2.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Model size class<\/th>\n<th>Weights at 4-bit<\/th>\n<th>Plan for, at 32k context<\/th>\n<th>Fits on<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>7\u20138B<\/td>\n<td>about 5 GB<\/td>\n<td>8\u201310 GB<\/td>\n<td>12\u201316 GB GPU<\/td>\n<\/tr>\n<tr>\n<td>12\u201314B<\/td>\n<td>about 8\u20139 GB<\/td>\n<td>13\u201316 GB<\/td>\n<td>16\u201324 GB GPU<\/td>\n<\/tr>\n<tr>\n<td>30B-class<\/td>\n<td>about 18\u201320 GB<\/td>\n<td>20\u201330 GB (mixture-of-experts at the low end)<\/td>\n<td>32\u201348 GB GPU, or 64 GB+ unified<\/td>\n<\/tr>\n<tr>\n<td>70B-class<\/td>\n<td>about 40\u201343 GB<\/td>\n<td>50\u201360 GB<\/td>\n<td>80 GB+ GPU, or 96\u2013128 GB unified<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The &quot;plan for&quot; column adds the <strong>KV cache<\/strong>, the memory reserved for context, which grows with context length and parallel slots. In the lab, a 30B mixture-of-experts coding model measured 20.3 GB at 32k with one slot, but about 33 GB (roughly 19 GB of weights plus 14 GB of KV cache) with four slots at 64k. On unified memory everything shares one pool, so leave a generous free floor. Our lab server was an NVIDIA DGX Spark, a compact desktop AI system built on the GB10 Grace Blackwell Superchip with 128 GB of unified memory shared by the CPU and GPU (the operating system reports roughly 120 GB as usable). We had already <a href=\"https:\/\/catharsis.net.au\/blog\/nvidea-dgx-spark-benchmarking\/\" target=\"_blank\" rel=\"noreferrer noopener\">benchmarked two of them linked together<\/a>; this guide needs only one, and any Linux server with enough GPU memory works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Software.<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>A current long-term-support Debian or Ubuntu release.<\/li>\n\n\n<li>A working NVIDIA driver (<code>nvidia-smi<\/code> lists the GPU) and the NVIDIA Container Toolkit.<\/li>\n\n\n<li>Docker Engine with the Compose v2 plugin (<code>docker compose<\/code>), for Open WebUI and Langfuse.<\/li>\n\n\n<li>Python 3.11 or later, for LiteLLM.<\/li>\n\n\n<li>For remote access, a domain name for TLS, or a firewall.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Time.<\/strong> Allow a day for Steps 1 to 6, plus half a day for tracing and lockdown.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-install-ollama-and-keep-it-private\">Step 1: Install Ollama and keep it private<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The install script creates an <code>ollama<\/code> service account and a systemd service; <code>OLLAMA_VERSION<\/code> pins the release, so upgrades happen when you choose.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -fsSL https:\/\/ollama.com\/install.sh | OLLAMA_VERSION=&lt;version&gt; sh\nsystemctl status ollama --no-pager\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Set memory settings explicitly; defaults change between releases. Current versions pick the default context from the GPU memory they detect: 4k below 24 GB, 32k up to 48 GB and 256k above that. Run <code>sudo systemctl edit ollama<\/code> and add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Service]\nEnvironment=&quot;OLLAMA_HOST=127.0.0.1:11434&quot;\nEnvironment=&quot;OLLAMA_NUM_PARALLEL=1&quot;\nEnvironment=&quot;OLLAMA_CONTEXT_LENGTH=32768&quot;\nEnvironment=&quot;OLLAMA_MAX_LOADED_MODELS=2&quot;\nEnvironment=&quot;OLLAMA_KEEP_ALIVE=30m&quot;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then <code>sudo systemctl restart ollama<\/code>. Why each one matters:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><code>OLLAMA_HOST<\/code> pins the listener to IPv4 loopback (the default, made explicit; a wildcard address exposes every model).<\/li>\n\n\n<li><code>OLLAMA_NUM_PARALLEL<\/code> is request slots per model. <strong>Each slot gets the full context<\/strong>, so memory grows with context \u00d7 slots. With one slot a second caller waits; in the lab the median wait was 18 seconds.<\/li>\n\n\n<li><code>OLLAMA_CONTEXT_LENGTH<\/code> is the context window, shared by prompt and answer.<\/li>\n\n\n<li><code>OLLAMA_MAX_LOADED_MODELS<\/code> (default three per GPU) stops a stray request loading a third model.<\/li>\n\n\n<li><code>OLLAMA_KEEP_ALIVE<\/code> (default five minutes) is how long an idle model stays loaded.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Caveat: <code>systemctl edit<\/code>, like any systemd reload (<code>enable<\/code> and <code>disable<\/code> included), can strip GPU access from running NVIDIA containers until they restart. Do it in a maintenance window and check each with <code>docker exec &lt;container&gt; nvidia-smi -L<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Or in a container.<\/strong> Pin the image by digest, publish on loopback only and reserve the GPU, or it quietly runs on the CPU. Then prefix the <code>ollama<\/code> commands below with <code>docker compose exec ollama<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services:\n  ollama:\n    image: ollama\/ollama@sha256:&lt;digest&gt;\n    restart: unless-stopped\n    ports:\n      - &quot;127.0.0.1:11434:11434&quot;\n    volumes:\n      - ollama:\/root\/.ollama\n    environment:\n      OLLAMA_NUM_PARALLEL: &quot;1&quot;\n      OLLAMA_CONTEXT_LENGTH: &quot;32768&quot;\n      OLLAMA_MAX_LOADED_MODELS: &quot;2&quot;\n      OLLAMA_KEEP_ALIVE: &quot;30m&quot;\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: all\n              capabilities: [gpu]\nvolumes:\n  ollama:\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> <code>ss -ltn | grep 11434<\/code> shows <code>127.0.0.1:11434<\/code> and nothing else. <code>systemctl show ollama --property=Environment<\/code> lists all five settings. From a second machine, <code>curl -m 5 http:\/\/&lt;server-address&gt;:11434<\/code> is refused or times out.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-choose-models-and-measure-them\">Step 2: Choose models and measure them<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start with two models: a general one for questions and drafting, and a coding one for code and agents. Here, a 12B Gemma model and a 30B mixture-of-experts Qwen coder, which activates only a few of its sub-networks per token, so it answers about as fast as a much smaller model. Check the Ollama library for current tags.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A model&#8217;s own context setting beats the server-wide one: in the lab, a 65,536-token value in a model&#8217;s settings quietly overrode the server limit and contributed to the freezes. So build tags with the context fixed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ollama pull gemma3:12b\nollama pull qwen3-coder:30b\nprintf 'FROM gemma3:12b\\nPARAMETER num_ctx 32768\\n' &gt; general.Modelfile\nprintf 'FROM qwen3-coder:30b\\nPARAMETER num_ctx 32768\\n' &gt; coder.Modelfile\nollama create general-32k -f general.Modelfile\nollama create coder-32k -f coder.Modelfile\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Send each model one request and read what it holds:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>for m in general-32k coder-32k; do\n  curl -s 127.0.0.1:11434\/api\/generate -d &quot;{\\&quot;model\\&quot;:\\&quot;$m\\&quot;,\\&quot;prompt\\&quot;:\\&quot;Hi\\&quot;,\\&quot;stream\\&quot;:false}&quot;\ndone\nollama ps\ncurl -s 127.0.0.1:11434\/api\/ps | python3 -m json.tool\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Write your memory budget down in Git: measured resident models plus a fixed free floor must fit. In the lab the resident pair measured 37.8 GB, and we keep at least 40 GB of the shared pool free. On a discrete GPU, leave roughly 10 per cent of VRAM spare.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> <code>ollama ps<\/code> shows <code>100% GPU<\/code> under PROCESSOR and 32768 under CONTEXT for both models (older versions lack that column; read <code>context_length<\/code> from <code>\/api\/ps<\/code>). <code>ollama show coder-32k --parameters<\/code> lists <code>num_ctx 32768<\/code>. If PROCESSOR shows any CPU share, fix the driver or the container&#8217;s GPU access before you go on.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-put-litellm-in-front-as-the-only-door\">Step 3: Put LiteLLM in front as the only door<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">LiteLLM is an open-source gateway that speaks the OpenAI API. Identity, model access, budgets and tracing all live there, so any path that skips it skips all of them. It keeps keys and spend logs in Postgres.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, the database, a service account and the secrets. LiteLLM&#8217;s admin key, the master key, must start with <code>sk-<\/code>. The salt key encrypts credentials stored in the database and must never change once set. Systemd reads the environment file as root before starting the service, so it can be root-only (mode 600).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y postgresql python3-venv\nsudo useradd --system --create-home --home-dir \/opt\/litellm --shell \/usr\/sbin\/nologin litellm\nsudo -u postgres createuser -P litellm\nsudo -u postgres createdb -O litellm litellm\nsudo install -d -m 750 -o root -g litellm \/etc\/litellm\nprintf '%s=%s\\n' \\\n  LITELLM_MASTER_KEY &quot;sk-$(openssl rand -hex 32)&quot; \\\n  LITELLM_SALT_KEY &quot;sk-$(openssl rand -hex 32)&quot; \\\n  DATABASE_URL 'postgresql:\/\/litellm:&lt;db-password&gt;@127.0.0.1:5432\/litellm' \\\n  | sudo install -m 600 -o root -g root \/dev\/stdin \/etc\/litellm\/litellm.env\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Install a pinned version into a virtual environment owned by the service account, because the proxy generates its Prisma database client on first start. The <code>extra_proxy<\/code> extra brings that client; without it the proxy fails at start-up.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo -u litellm python3 -m venv \/opt\/litellm\/venv\nsudo -u litellm \/opt\/litellm\/venv\/bin\/pip install 'litellm[proxy,extra_proxy]==&lt;version&gt;'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Write <code>\/etc\/litellm\/config.yaml<\/code> (owner root, group <code>litellm<\/code>, mode 640):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>model_list:\n  - model_name: general\n    litellm_params:\n      model: ollama_chat\/general-32k\n      api_base: http:\/\/127.0.0.1:11434\n    model_info:\n      max_input_tokens: 26000\n  - model_name: coder\n    litellm_params:\n      model: ollama_chat\/coder-32k\n      api_base: http:\/\/127.0.0.1:11434\n    model_info:\n      max_input_tokens: 26000\nrouter_settings:\n  enable_pre_call_checks: true\nlitellm_settings:\n  drop_params: true\n  request_timeout: 600\ngeneral_settings:\n  master_key: os.environ\/LITELLM_MASTER_KEY\n  database_url: os.environ\/DATABASE_URL\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The deliberate choices:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><code>ollama_chat\/<\/code> uses Ollama&#8217;s chat endpoint, which returns native tool calls; with <code>ollama\/<\/code>, agents get text instead.<\/li>\n\n\n<li><code>max_input_tokens: 26000<\/code> leaves 6,768 tokens of the 32k window for the answer. On its own it is only metadata; <code>enable_pre_call_checks<\/code> and Step 5 make it count.<\/li>\n\n\n<li>No <code>max_parallel_requests<\/code>: current releases reject a request over that cap at once with a <code>429<\/code> instead of queueing it. With one slot, Ollama already queues the second caller.<\/li>\n\n\n<li>No <code>fallbacks<\/code> or <code>default_fallbacks<\/code> (see Step 8).<\/li>\n\n\n<li>One <code>litellm_settings<\/code> block only. Later steps add to it; a second <code>litellm_settings:<\/code> line silently replaces the first.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Run it as a systemd service bound to loopback. Save this as <code>\/etc\/systemd\/system\/litellm.service<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=LiteLLM gateway\nAfter=network-online.target postgresql.service ollama.service\nWants=network-online.target\n\n[Service]\nUser=litellm\nGroup=litellm\nEnvironmentFile=\/etc\/litellm\/litellm.env\nExecStart=\/opt\/litellm\/venv\/bin\/litellm --config \/etc\/litellm\/config.yaml --host 127.0.0.1 --port 4000\nRestart=on-failure\nNoNewPrivileges=true\n\n[Install]\nWantedBy=multi-user.target\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then <code>sudo systemctl daemon-reload &amp;&amp; sudo systemctl enable --now litellm<\/code> (the Step 1 GPU caveat applies). The first start takes a minute while the proxy creates its database tables.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> <code>curl -s 127.0.0.1:4000\/health\/liveliness<\/code> answers that it is alive, and <code>curl -s 127.0.0.1:4000\/health\/readiness<\/code> reports the database as connected. <code>ss -ltn | grep 4000<\/code> shows <code>127.0.0.1:4000<\/code> only. <code>journalctl -u litellm -n 50<\/code> shows no Prisma errors.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-give-every-app-its-own-key\">Step 4: Give every app its own key<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Virtual keys<\/strong> are LiteLLM&#8217;s per-app keys, each with an alias, models, rate limits and an optional budget, so you can see who sent what, limit each and revoke one alone. Keep the master key for administration: in the lab, one load test that fell back to it set off the &quot;admin key used&quot; alert, so even tests got their own key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mint keys on the server, in a root shell, so the master key never crosses the network:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set -a; . \/etc\/litellm\/litellm.env; set +a\ncurl -s 127.0.0.1:4000\/key\/generate \\\n  -H &quot;Authorization: Bearer $LITELLM_MASTER_KEY&quot; -H 'Content-Type: application\/json' \\\n  -d '{&quot;key_alias&quot;:&quot;chat-ui&quot;,&quot;models&quot;:[&quot;general&quot;,&quot;coder&quot;],&quot;rpm_limit&quot;:60,&quot;tpm_limit&quot;:200000}'\ncurl -s 127.0.0.1:4000\/key\/generate \\\n  -H &quot;Authorization: Bearer $LITELLM_MASTER_KEY&quot; -H 'Content-Type: application\/json' \\\n  -d '{&quot;key_alias&quot;:&quot;invoice-app&quot;,&quot;models&quot;:[&quot;general&quot;],&quot;rpm_limit&quot;:20,&quot;tpm_limit&quot;:100000,&quot;max_budget&quot;:1,&quot;budget_duration&quot;:&quot;30d&quot;}'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Always list the models: an empty list means every model, including any cloud model added later. Give batch keys lower rate limits than interactive ones, so a runaway loop can&#8217;t fill the queue. <code>max_budget<\/code> is only a tripwire for cloud spend. Each key is shown once; store it only in that client&#8217;s configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> each response contains a <code>key<\/code>. A call with the invoice key to <code>coder<\/code> is refused as not allowed for that key. With <code>APP_KEY<\/code> set to one app&#8217;s key, <code>curl -s 127.0.0.1:4000\/v1\/models -H &quot;Authorization: Bearer $APP_KEY&quot;<\/code> lists only that key&#8217;s models.<\/p>\n\n\n\n<div class=\"wp-block-group has-pale-cyan-blue-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:32px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Thinking about private AI for your organisation?<\/strong> We design, build and secure on-premises AI platforms for Australian organisations, with the guardrails this series describes built in from the start.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/local-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-qs-mid\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Private AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-stop-oversized-prompts-before-they-reach-the-gpu\">Step 5: Stop oversized prompts before they reach the GPU<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By default Ollama doesn&#8217;t refuse a prompt bigger than the context window. It drops what doesn&#8217;t fit, logs a truncation warning and answers anyway, so an agent silently loses part of its own reasoning. You want a clear refusal before the GPU does any work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The quick option is already in the config: with <code>enable_pre_call_checks: true<\/code>, LiteLLM won&#8217;t send a request over <code>max_input_tokens<\/code>. But its counter is generic, not the model&#8217;s tokenizer, and older releases ignored tool definitions: in the lab, a short message carrying a dozen tool schemas counted as 11 tokens but cost 3,540.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The better option is a small pre-call hook, saved as <code>\/etc\/litellm\/context_guard.py<\/code> next to <code>config.yaml<\/code> (owner root, group <code>litellm<\/code>, mode 640):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import json\n\nfrom fastapi import HTTPException\nfrom litellm import token_counter\nfrom litellm.integrations.custom_logger import CustomLogger\n\nBUDGETS = {&quot;general&quot;: 26000, &quot;coder&quot;: 26000}\nMARGIN = 1.15  # safety factor; calibrate per model against usage.prompt_tokens\n\n\nclass ContextGuard(CustomLogger):\n    async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):\n        messages = data.get(&quot;messages&quot;)\n        if not messages:\n            return data\n        n = token_counter(model=&quot;&quot;, messages=messages)\n        if data.get(&quot;tools&quot;):\n            n += token_counter(model=&quot;&quot;, text=json.dumps(data[&quot;tools&quot;]))\n        n = int(n * MARGIN)\n        budget = BUDGETS.get(data.get(&quot;model&quot;), 13000)\n        if n &gt; budget:\n            raise HTTPException(\n                status_code=400,\n                detail=f&quot;context_length_exceeded: about {n} tokens is over the &quot;\n                f&quot;{budget}-token budget; shorten the input&quot;,\n            )\n        return data\n\n\nguard_instance = ContextGuard()\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Register the instance (not the class) by adding this line to the existing <code>litellm_settings<\/code> block, then restart LiteLLM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  callbacks: context_guard.guard_instance\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The hook returns <code>data<\/code> to allow a request and raises an <code>HTTPException<\/code> to refuse it. Its generic counter under-counts digit-heavy text, where these models spend about one token per digit, so in production count with each model&#8217;s own tokenizer and chat template (<a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">Part 4<\/a>) and compare with each response&#8217;s <code>usage.prompt_tokens<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> with <code>APP_KEY<\/code> set, send a prompt of about 40,000 words, then a short message with many tool definitions.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 - &lt;&lt;'EOF'\nimport json\ntool = lambda i: {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: {&quot;name&quot;: f&quot;tool_{i}&quot;,\n    &quot;description&quot;: &quot;Look up a record in the sample system. &quot; * 40,\n    &quot;parameters&quot;: {&quot;type&quot;: &quot;object&quot;, &quot;properties&quot;: {&quot;id&quot;: {&quot;type&quot;: &quot;string&quot;}}}}}\njson.dump({&quot;model&quot;: &quot;general&quot;, &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;word &quot; * 40000}]},\n          open(&quot;big.json&quot;, &quot;w&quot;))\njson.dump({&quot;model&quot;: &quot;general&quot;, &quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;hi&quot;}],\n           &quot;tools&quot;: [tool(i) for i in range(150)]}, open(&quot;tools.json&quot;, &quot;w&quot;))\nEOF\nfor f in big.json tools.json; do\n  curl -s -w '\\nHTTP %{http_code}\\n' 127.0.0.1:4000\/v1\/chat\/completions \\\n    -H &quot;Authorization: Bearer $APP_KEY&quot; -H 'Content-Type: application\/json' -d @&quot;$f&quot;\ndone\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both come back within milliseconds as <code>HTTP 400<\/code> with <code>context_length_exceeded<\/code> in the error message, and <code>journalctl -u ollama --since &quot;5 min ago&quot;<\/code> shows no new <code>POST \/api\/chat<\/code> line. With only the built-in check, the first is refused; on older releases the second gets through.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-connect-a-chat-ui-and-your-first-app\">Step 6: Connect a chat UI and your first app<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Point <strong>Open WebUI<\/strong> at the gateway with its own key, never at Ollama, so every chat is attributed and traced. Because LiteLLM listens only on loopback, run Open WebUI with host networking, also on loopback, with its secrets in a mode-600 file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo install -d -m 750 \/srv\/open-webui\nprintf '%s=%s\\n' \\\n  OPENAI_API_KEY '&lt;chat-ui-key-from-step-4&gt;' \\\n  WEBUI_SECRET_KEY &quot;$(openssl rand -hex 32)&quot; \\\n  | sudo install -m 600 \/dev\/stdin \/srv\/open-webui\/webui.env\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then <code>\/srv\/open-webui\/compose.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services:\n  open-webui:\n    image: ghcr.io\/open-webui\/open-webui@sha256:&lt;digest&gt;\n    restart: unless-stopped\n    network_mode: host\n    env_file: [webui.env]\n    environment:\n      HOST: &quot;127.0.0.1&quot;\n      PORT: &quot;8080&quot;\n      ENABLE_OLLAMA_API: &quot;false&quot;\n      OPENAI_API_BASE_URL: &quot;http:\/\/127.0.0.1:4000\/v1&quot;\n    volumes:\n      - open-webui:\/app\/backend\/data\nvolumes:\n  open-webui:\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Start it with <code>sudo docker compose -f \/srv\/open-webui\/compose.yaml up -d<\/code>; the first account you create becomes the administrator. Open WebUI saves most of these settings to its database on first start, and that copy then wins, so make later changes in its admin settings.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For your own apps, anything that speaks the OpenAI API works, such as the <code>openai<\/code> Python package:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\n\nfrom openai import OpenAI\n\nkey = os.environ[&quot;APP_KEY&quot;]\nclient = OpenAI(base_url=&quot;http:\/\/127.0.0.1:4000\/v1&quot;, api_key=key)\nreply = client.chat.completions.create(\n    model=&quot;general&quot;,\n    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Write a two-line status update.&quot;}],\n)\nprint(reply.choices[0].message.content)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apps on other machines use the HTTPS address from Step 8.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> <code>ss -ltn | grep 8080<\/code> shows <code>127.0.0.1:8080<\/code>. Open WebUI lists only <code>general<\/code> and <code>coder<\/code>, and a chat gets an answer. In LiteLLM&#8217;s spend logs the chat appears under <code>chat-ui<\/code> and the script&#8217;s call under its own alias.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-see-what-is-happening\">Step 7: See what is happening<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start with the gateway: <code>journalctl -u litellm -f<\/code> for live errors, the spend logs for every call&#8217;s key alias, model and tokens, and the admin UI at <code>\/ui<\/code> for usage per key. Ollama&#8217;s log is a second witness: a <code>POST \/api\/chat<\/code> with no matching gateway request means something found a side door.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For per-request detail, add <strong>Langfuse<\/strong>, an open-source tool that records each call as a trace with prompt, answer, tokens and timing. Langfuse v3 needs Postgres, ClickHouse, Redis, S3-compatible blob storage and a worker; the official Docker Compose file brings them all. Before the first start:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Replace every value marked <code># CHANGEME<\/code> with output from <code>openssl rand -hex 32<\/code> (the 64 hex characters the encryption key needs).<\/li>\n\n\n<li>Publish the web app on <code>127.0.0.1:3000:3000<\/code> and the object store on <code>127.0.0.1:9090:9000<\/code>; the other services are already on loopback.<\/li>\n\n\n<li>Remove the Postgres port mapping (<code>127.0.0.1:5432:5432<\/code>): it clashes with the LiteLLM database, and the containers reach each other on their own network.<\/li>\n\n\n<li>Cap Docker&#8217;s log size in the daemon settings.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Then connect LiteLLM. Current releases need the Langfuse Python SDK v4 in the gateway&#8217;s venv, and that SDK needs Langfuse server 3.63.0 or newer; check both for your pinned release.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo -u litellm \/opt\/litellm\/venv\/bin\/pip install 'langfuse&gt;=4.7,&lt;5'\nprintf '%s=%s\\n' \\\n  LANGFUSE_PUBLIC_KEY '&lt;public-key-from-langfuse&gt;' \\\n  LANGFUSE_SECRET_KEY '&lt;secret-key-from-langfuse&gt;' \\\n  LANGFUSE_HOST 'http:\/\/127.0.0.1:3000' \\\n  | sudo tee -a \/etc\/litellm\/litellm.env &gt; \/dev\/null\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add the callbacks to the same <code>litellm_settings<\/code> block. In the lab they once sat in the wrong section, and LiteLLM ignored them silently. The block now reads:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>litellm_settings:\n  drop_params: true\n  request_timeout: 600\n  callbacks: context_guard.guard_instance\n  success_callback: [&quot;langfuse&quot;]\n  failure_callback: [&quot;langfuse&quot;]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restart LiteLLM. Then watch refusals per key (a run of <code>context_length_exceeded<\/code> means a client&#8217;s prompt has grown), time to first token, queue waits, unexpected models in <code>ollama ps<\/code>, free memory against your floor, and GPU temperature. In the lab, one ordinary test run took the sensors from 48 \u00b0C to 95 \u00b0C in four minutes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> the Langfuse containers stay up for an hour without restarts, <code>ss -ltn<\/code> shows ports 3000 and 9090 on <code>127.0.0.1<\/code> only, and one chat through the gateway appears as a trace within a minute.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-lock-it-down\">Step 8: Lock it down<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything listens on loopback; add one controlled way in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Remote access through TLS.<\/strong> Keys are bearer tokens, so never send them over plain HTTP. Put a TLS reverse proxy in front that passes only the API and the liveness check, keeping LiteLLM&#8217;s admin UI and management routes off the network. A Caddy example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ai.example.com {\n\t@api path \/v1\/* \/health\/liveliness\n\thandle @api {\n\t\treverse_proxy 127.0.0.1:4000\n\t}\n\thandle {\n\t\trespond 403\n\t}\n}\n\nchat.example.com {\n\treverse_proxy 127.0.0.1:8080\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An allow-list beats blocking admin paths one by one, because new releases add management routes. Open WebUI has its own logins. Or publish nothing and let a host firewall allow only the networks that need it (allow SSH first). Ports Docker publishes on all interfaces bypass host firewalls such as <code>ufw<\/code>, another reason to publish every container port on <code>127.0.0.1<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>No silent cloud fallback.<\/strong> A LiteLLM fallback retries a failed or slow request on another model for every caller, because it belongs to the model, not the key. A local model with a short timeout and a cloud fallback quietly sends whole prompts to the cloud whenever the local slot is busy, and free cloud tiers may log or train on them. Let local models fall back only to local ones, enforced by a pre-commit check on <code>fallbacks<\/code> and <code>default_fallbacks<\/code>. Grant any cloud model to one key, for public data only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Secrets out of reach.<\/strong> Environment files are root-only and stay out of Git (commit placeholder templates). Nothing a person, app or AI agent can read holds the master key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Least privilege for AI coding agents.<\/strong> Treat one like a new contractor: an unprivileged account with no <code>sudo<\/code> and no <code>docker<\/code> group membership (which equals full control of the host), no access to secret files, and every change through Git and a review, never straight onto the server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Write the data rule down:<\/strong> &quot;Data never leaves the server, and every model call goes through the gateway.&quot; Loopback binding, per-key model lists and the fallback check enforce it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> from another machine, ports 11434, 4000, 8080, 3000, 9090 and 5432 are refused or time out, while <code>https:\/\/ai.example.com\/health\/liveliness<\/code> answers. <code>https:\/\/ai.example.com\/ui<\/code> and <code>https:\/\/ai.example.com\/key\/list<\/code> return 403. A commit that adds a local-to-cloud fallback is refused. As the agent&#8217;s account, <code>cat \/etc\/litellm\/litellm.env<\/code> fails with &quot;Permission denied&quot;.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-keep-it-running\">Step 9: Keep it running<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pin everything:<\/strong> Ollama, LiteLLM, and every image by digest (<code>image@sha256:...<\/code>), never <code>:latest<\/code>. In the lab, a container recreated from a floating tag silently upgraded the model server days before the first freeze. <code>docker compose config --images<\/code> should list only <code>@sha256:<\/code> references.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Keep config in Git:<\/strong> the Ollama override, LiteLLM config and hook, units, compose files, env templates, Modelfiles and memory budget, with a pre-commit hook that refuses unpinned images, private keys and cloud fallbacks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Change through a deploy, not by hand.<\/strong> A short script pulls the repository, stops on any hand edit to the live files, copies the new files in and restarts only what changed, reloading systemd only for unit changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Update in a controlled way.<\/strong> Read the release notes and upgrade one component at a time in a maintenance window; LiteLLM updates its database schema when the new version starts, so back up first. Smoke-test streamed and plain answers, tools, an oversized prompt and one call per app. Soak for a day, with the previous pin ready for rollback.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Back up, and test the restore.<\/strong> Back up nightly to a separate target, with an alert when a backup fails or doesn&#8217;t run: the LiteLLM database (<code>pg_dump<\/code>), Open WebUI&#8217;s data volume, Langfuse&#8217;s databases if you keep traces, and the config repository. Models can be rebuilt from the Modelfiles. Test restores into a scratch database; an untested backup is a hope.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Watch from outside.<\/strong> A hard freeze takes the server&#8217;s own monitoring with it; in the lab an on-board watchdog hung with the host. A separate device should check the health address every minute and alert after a few misses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Watch temperatures.<\/strong> Long generations are sustained full load: record GPU and system temperatures every few seconds and check airflow before adding work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check:<\/strong> after a deploy the config checkout is clean, and a second deploy of the same commit changes nothing. Unplugging the server&#8217;s network raises the outside alert. A gateway pointed at a restored database lists your keys at <code>\/key\/list<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"best-practice-checklist\">Best-practice checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Ollama on loopback, with slots, context, loaded models and keep-alive set; <code>num_ctx<\/code> fixed in tags; measured models plus a free floor fit.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> LiteLLM is the only way in, on loopback, unprivileged; only root can read the master key.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Every app, UI and test has its own key, model list and rate limits; Open WebUI&#8217;s Ollama connection is off.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Oversized prompts are refused, tool definitions included; local models use <code>ollama_chat\/<\/code>.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> No local-to-cloud fallback, enforced at commit.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Every call leaves a log row and a trace; refusals per key raise an alert.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Remote access only through TLS with only the API allowed, or a firewall.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> AI coding agents run unprivileged, can&#8217;t read secrets and change things through Git.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Pinned versions, config in Git, smoke-tested and soaked upgrades.<\/li>\n\n\n<li><input type=\"checkbox\" disabled=\"\" \/> Tested nightly backups, an outside heartbeat and recorded temperatures.<\/li>\n\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting\">Troubleshooting<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Symptom<\/th>\n<th>Likely cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Server freezes under load, no kernel error<\/td>\n<td>KV cache: too many slots \u00d7 too large a context, or a model&#8217;s own <code>num_ctx<\/code> winning<\/td>\n<td>One slot, <code>num_ctx<\/code> fixed in tags, a free floor, an outside heartbeat<\/td>\n<\/tr>\n<tr>\n<td>Answers cut off, or the start of a chat forgotten<\/td>\n<td>Prompt truncated to fit the window, or <code>finish_reason<\/code> is <code>length<\/code><\/td>\n<td>The Step 5 hook; room for the answer<\/td>\n<\/tr>\n<tr>\n<td>An agent never gets tool calls<\/td>\n<td>Model mapped as <code>ollama\/<\/code>, or the model lacks tool support<\/td>\n<td>Map it as <code>ollama_chat\/&lt;tag&gt;<\/code>; test a request with <code>tools<\/code><\/td>\n<\/tr>\n<tr>\n<td>Slow first replies, or <code>ollama ps<\/code> keeps changing<\/td>\n<td>Models unloaded, or more models called than fit (swapping)<\/td>\n<td>Raise <code>OLLAMA_KEEP_ALIVE<\/code>; narrow key model lists; check nothing calls Ollama directly<\/td>\n<\/tr>\n<tr>\n<td>GPU containers fail with &quot;Failed to initialize NVML&quot;<\/td>\n<td>A systemd reload removed their device access<\/td>\n<td>Restart them; reload in maintenance windows; consider the NVIDIA Container Toolkit&#8217;s CDI mode<\/td>\n<\/tr>\n<tr>\n<td>Langfuse shows nothing<\/td>\n<td>Callbacks in the wrong section, wrong SDK or server version, wrong host or keys<\/td>\n<td>Fix them; read the gateway log for callback errors<\/td>\n<\/tr>\n<tr>\n<td>Blank answers from a reasoning model<\/td>\n<td>It spent all of <code>max_tokens<\/code> thinking<\/td>\n<td>Raise <code>max_tokens<\/code> or reduce thinking<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"where-to-go-deeper\">Where to go deeper<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Running Local AI in Production: The Complete Series Guide<\/a>: all 11 parts, including routing, agents and workflows.<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/llm-gateway-one-door-for-every-model\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Control Access to Local AI Models? Use a Gateway<\/a>: Steps 3, 4 and 8 in depth.<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-ai-server-freezes-unified-memory\/\" target=\"_blank\" rel=\"noreferrer noopener\">Why Does a Local AI Server Freeze? Sizing Memory for LLMs<\/a>: Steps 1 and 2 in depth.<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/local-llm-context-budget\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Happens When a Prompt Is Too Big for a Local LLM?<\/a>: Step 5 in depth.<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/gitops-for-a-gpu-ai-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Do You Keep an AI Server Stable? GitOps for Local AI<\/a>: Step 9 in depth.<\/li>\n\n\n<li><a href=\"https:\/\/catharsis.net.au\/blog\/observability-for-local-ai\/\" target=\"_blank\" rel=\"noreferrer noopener\">What Is Your Local AI Server Actually Doing? Observability Basics<\/a>: Step 7 in depth.<\/li>\n\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-light-green-cyan-background-color has-background\" style=\"padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px;margin-top:40px;margin-bottom:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<h2 class=\"wp-block-heading\">Planning private AI?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Catharsis Cyber Security is a Sydney cyber security company. We design, build and secure private AI platforms like the one in this lab, and help you decide where local or cloud AI fits.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/contact-us\/?type=contact&amp;service=local-ai&amp;utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-qs-contact\" target=\"_blank\" rel=\"noreferrer noopener\">Talk to us about AI<\/a><\/div>\n\n\n<div class=\"wp-block-button is-style-outline is-style-outline--1\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/secure-ai\/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=local-ai-series&amp;utm_content=part-qs-end\" target=\"_blank\" rel=\"noreferrer noopener\">Explore Secure AI<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group has-background\" style=\"background-color:#f4f6f8;padding-top:28px;padding-right:32px;padding-bottom:28px;padding-left:32px\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-40de7cf3 wp-block-group-is-layout-constrained\">\n\n<p class=\"wp-block-paragraph\"><strong>Keep reading the series<\/strong><\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n\n<div class=\"wp-block-button is-style-outline is-style-outline--2\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/running-local-ai-in-production-series-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\">Series guide<\/a><\/div>\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/catharsis.net.au\/blog\/why-run-ai-on-your-own-hardware\/\" target=\"_blank\" rel=\"noreferrer noopener\">Start the series: Part 1 \u2192<\/a><\/div>\n\n<\/div>\n\n<\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>A step-by-step guide to running AI on your own Linux server: Ollama kept private, LiteLLM as the only way in, a key per app, a chat UI, tracing and a locked-down server. Every step ends with a check.<\/p>\n","protected":false},"author":2,"featured_media":1158,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[69],"tags":[83,133,85,81,84,131,75,80,121,77,132],"class_list":["post-1159","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-ai-lab","tag-how-to","tag-langfuse","tag-litellm","tag-llm-gateway","tag-local-ai","tag-local-llm","tag-ollama","tag-open-webui","tag-private-ai","tag-self-hosted-ai"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog<\/title>\n<meta name=\"description\" content=\"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog\" \/>\n<meta property=\"og:description\" content=\"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/\" \/>\n<meta property=\"og:site_name\" content=\"Catharsis | Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-28T23:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-11-03T09:28:19+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1280\" \/>\n\t<meta property=\"og:image:height\" content=\"720\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Sajal Jain\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Sajal Jain\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"27 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/\"},\"author\":{\"name\":\"Sajal Jain\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\"},\"headline\":\"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide\",\"datePublished\":\"2026-10-28T23:00:00+00:00\",\"dateModified\":\"2026-11-03T09:28:19+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/\"},\"wordCount\":4083,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-qs.jpg\",\"keywords\":[\"AI lab\",\"how-to\",\"Langfuse\",\"LiteLLM\",\"LLM gateway\",\"local AI\",\"local LLM\",\"Ollama\",\"Open WebUI\",\"private AI\",\"self-hosted AI\"],\"articleSection\":[\"Artificial Intelligence\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/\",\"name\":\"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-qs.jpg\",\"datePublished\":\"2026-10-28T23:00:00+00:00\",\"dateModified\":\"2026-11-03T09:28:19+00:00\",\"description\":\"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#primaryimage\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-qs.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/local-ai-series-part-qs.jpg\",\"width\":1280,\"height\":720,\"caption\":\"Close-up of memory chips on a computer board\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/local-ai-litellm-setup-guide\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"name\":\"Catharsis | Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#organization\",\"name\":\"Catharsis | Blog\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/catharsis_au.jpg\",\"width\":215,\"height\":60,\"caption\":\"Catharsis | Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/#\\\/schema\\\/person\\\/f0826efb92a7aaf8d46b24715990e099\",\"name\":\"Sajal Jain\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"contentUrl\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/01\\\/Sajal-copy-96x96.jpg\",\"caption\":\"Sajal Jain\"},\"description\":\"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management\",\"url\":\"https:\\\/\\\/catharsis.net.au\\\/blog\\\/author\\\/sajal-jain\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog","description":"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/","og_locale":"en_US","og_type":"article","og_title":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog","og_description":"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.","og_url":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/","og_site_name":"Catharsis | Blog","article_published_time":"2026-10-28T23:00:00+00:00","article_modified_time":"2026-11-03T09:28:19+00:00","og_image":[{"width":1280,"height":720,"url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg","type":"image\/jpeg"}],"author":"Sajal Jain","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Sajal Jain","Est. reading time":"27 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#article","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/"},"author":{"name":"Sajal Jain","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099"},"headline":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide","datePublished":"2026-10-28T23:00:00+00:00","dateModified":"2026-11-03T09:28:19+00:00","mainEntityOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/"},"wordCount":4083,"commentCount":0,"publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg","keywords":["AI lab","how-to","Langfuse","LiteLLM","LLM gateway","local AI","local LLM","Ollama","Open WebUI","private AI","self-hosted AI"],"articleSection":["Artificial Intelligence"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/","url":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/","name":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide - Catharsis | Blog","isPartOf":{"@id":"https:\/\/catharsis.net.au\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#primaryimage"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#primaryimage"},"thumbnailUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg","datePublished":"2026-10-28T23:00:00+00:00","dateModified":"2026-11-03T09:28:19+00:00","description":"How do you set up local AI? A step-by-step guide: Ollama on loopback, LiteLLM as the only door, a key per app, a chat UI, tracing and a locked-down server.","breadcrumb":{"@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#primaryimage","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2026\/11\/local-ai-series-part-qs.jpg","width":1280,"height":720,"caption":"Close-up of memory chips on a computer board"},{"@type":"BreadcrumbList","@id":"https:\/\/catharsis.net.au\/blog\/local-ai-litellm-setup-guide\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/catharsis.net.au\/blog\/"},{"@type":"ListItem","position":2,"name":"How Do You Set Up Local AI? A Step-by-Step LiteLLM Guide"}]},{"@type":"WebSite","@id":"https:\/\/catharsis.net.au\/blog\/#website","url":"https:\/\/catharsis.net.au\/blog\/","name":"Catharsis | Blog","description":"","publisher":{"@id":"https:\/\/catharsis.net.au\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/catharsis.net.au\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/catharsis.net.au\/blog\/#organization","name":"Catharsis | Blog","url":"https:\/\/catharsis.net.au\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/catharsis_au.jpg","width":215,"height":60,"caption":"Catharsis | Blog"},"image":{"@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/catharsis.net.au\/blog\/#\/schema\/person\/f0826efb92a7aaf8d46b24715990e099","name":"Sajal Jain","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","url":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","contentUrl":"https:\/\/catharsis.net.au\/blog\/wp-content\/uploads\/2019\/01\/Sajal-copy-96x96.jpg","caption":"Sajal Jain"},"description":"Cyber Security Enthusiast | OSCP | CEH | CHFI | CNSS | Master of IT Security &amp; Master of IT Management","url":"https:\/\/catharsis.net.au\/blog\/author\/sajal-jain\/"}]}},"_links":{"self":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1159","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/comments?post=1159"}],"version-history":[{"count":2,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1159\/revisions"}],"predecessor-version":[{"id":1223,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/posts\/1159\/revisions\/1223"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media\/1158"}],"wp:attachment":[{"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/media?parent=1159"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/categories?post=1159"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/catharsis.net.au\/blog\/wp-json\/wp\/v2\/tags?post=1159"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}