Running Local LLMs in Production
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Ollama 0.34.4, vLLM 0.30.0, llama.cpp v0.5.0 and Docker Compose v2 with the NVIDIA Container Toolkit, as checked on 2026-09-28 (see the Running LLMs Locally baseline). It covers one team’s GPU server or small cluster. Serving many models at scale — autoscaling policies, multi-tenant capacity planning, evaluation in production and SLO dashboards — is the subject of LLMOps: Serving at Scale; multi-node GPU cluster design is out of scope.
Containerising Ollama and vLLM
GPU containers need the host’s NVIDIA driver plus the NVIDIA Container Toolkit, which lets Docker pass GPUs into
containers (--gpus all on the CLI, a device reservation in Compose). A DocsAssistant stack with Ollama for the
laptop-class models, vLLM for the large one, and an authenticating proxy as the only published port:
services:
ollama:
image: ollama/ollama:latest
volumes:
- ollama-models:/root/.ollama # weights survive container restarts
environment:
OLLAMA_CONTEXT_LENGTH: "32768"
OLLAMA_KEEP_ALIVE: "24h"
OLLAMA_NO_CLOUD: "1" # no prompts to Ollama's cloud
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 30s
timeout: 10s
retries: 5
# no "ports:" -- reachable only on the Compose network
vllm:
image: vllm/vllm-openai:latest
command: ["meta-llama/Llama-3.1-8B-Instruct", "--max-model-len", "32768"]
ipc: host
environment:
HF_TOKEN: ${HF_TOKEN}
VLLM_API_KEY: ${VLLM_API_KEY}
volumes:
- hf-cache:/root/.cache/huggingface
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["1"]
capabilities: [gpu]
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
start_period: 600s # first start downloads and loads the weights
gateway:
image: nginx:stable
ports:
- "443:443"
volumes:
- ./nginx:/etc/nginx/conf.d:ro
- ./tls:/etc/nginx/tls:ro
depends_on:
ollama: {condition: service_healthy}
vllm: {condition: service_healthy}
volumes:
ollama-models:
hf-cache:
The gateway configuration is the nginx key check on
LLM Gateways; a LiteLLM container
can take its place. Secrets come from the environment (${HF_TOKEN}), never from the file. Compose itself — how services, networks and volumes work — is covered on Compose
for Local Development and Compose in Practice, and
Storage: Volumes, Bind Mounts and tmpfs explains the
volume choices. See Ollama — Docker,
vLLM — Docker and
Docker — GPU support in Compose.
Health checks
A model server is not ready when its process starts: it may still be downloading tens of gigabytes and then loading them into GPU memory. Use each engine’s own endpoint and give it a generous start period:
| Engine | Health / readiness check |
|---|---|
Ollama |
|
llama.cpp |
|
vLLM |
|
NVIDIA NIM |
|
In Kubernetes, map readiness to these endpoints and set a startupProbe long enough for the first load, so the
liveness probe does not kill a pod that is still reading weights. See the
llama.cpp server README,
vLLM — Online serving and
Ollama — API.
Model-weight caching and volumes
Weights are the largest artefact in the system — often larger than every container image combined — so treat them like data, not like image layers:
-
Mount a persistent volume for each engine’s cache:
/root/.ollama(Ollama),/root/.cache/huggingface(vLLM, SGLang, llama.cpp’s-hfcache),/opt/nim/.cache(NIM). -
Pre-pull in a controlled step (
ollama pull,hf download, NIM’sdownload-to-cache) rather than on the first user request, and keep a copy on internal storage so a pod reschedule or an air-gapped site does not depend on the public Hub. -
Pin versions: a model tag such as
llama3.1:8bor a Hub repository’s default branch can change underneath you. Pin the digest or a revision for reproducibility. -
Keep the cache on fast local disk (NVMe): load time from a network share dominates cold starts.
Do not bake large weights into images; it makes every image push, pull and scan slow. See Image Best Practices and Supply-Chain Security and vLLM — Kubernetes.
Autoscaling limits
Autoscaling a GPU model server is much less elastic than scaling a web service:
-
Cold start is minutes, not seconds: schedule a GPU node, pull the image, load tens of gigabytes into VRAM, and warm the caches. Scale on leading signals (queue depth, running requests, KV-cache usage), not on CPU, and keep a warm minimum.
-
GPUs are scarce and indivisible: a replica needs a whole GPU (or several, with tensor parallelism); scaling from one to two doubles the cost in one step, and the capacity may simply not be available.
-
Scale-to-zero trades cost for a long first response; acceptable for batch jobs, rarely for interactive use.
-
Within one replica, continuous batching already absorbs load up to the goodput knee (see goodput); add replicas only past it.
The Kubernetes mechanics are covered on Orchestration: Swarm Mode and Kubernetes.
Securing endpoints
Treat a model server like a database: never on the public internet, always behind authentication.
-
Authentication: most engines are unauthenticated by default, and vLLM’s
--api-keycovers only its/v1-style routes. Terminate client authentication in a gateway or reverse proxy with per-client keys. -
TLS: terminate TLS at that proxy; inside a cluster, use the service mesh or mTLS if your platform provides it.
-
Network isolation: only the gateway may reach the engines. In Kubernetes, a
NetworkPolicyexpresses that:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: vllm-only-from-gateway
spec:
podSelector:
matchLabels: {app: docsassistant-vllm}
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: {app: llm-gateway}
ports:
- {protocol: TCP, port: 8000}
-
Egress: block outbound traffic from the engines except to the model registry you mirror, so a compromised or misconfigured server cannot send prompts elsewhere (and disable engine cloud features such as Ollama’s).
-
Model-management endpoints: pulling, deleting and loading models (Ollama’s
/api/pulland/api/delete, Docker Model Runner’s whole API, llama.cpp’s/modelsrouter endpoints) must never be reachable by ordinary clients.
Prompt-injection, data-leakage and tool-abuse risks of the applications built on these models are covered in Agent Guardrails & Safety and the AI Security & Responsible AI sub-section (start here); container hardening in Docker Security. See vLLM — Security.
Monitoring
vLLM serves Prometheus metrics on /metrics; llama.cpp does so when started with --metrics. The metrics that
matter map directly onto the definitions on Inference
Optimization:
| Question | vLLM | llama.cpp |
|---|---|---|
Is it overloaded? |
|
|
Time to first token |
|
— |
Time per output token |
|
|
End-to-end latency |
|
— |
Throughput |
|
|
Memory pressure |
|
— |
# prometheus.yml
scrape_configs:
- job_name: vllm
static_configs:
- targets: ["vllm:8000"]
- job_name: llamacpp
static_configs:
- targets: ["llamacpp:8080"]
A p95 TTFT alert, for example:
histogram_quantile(0.95, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))) > 2
Add GPU metrics (utilisation, memory, temperature) from NVIDIA’s DCGM exporter. Prometheus itself — scraping,
PromQL, recording and alerting rules, Kubernetes monitoring — is covered in
Prometheus & Monitoring, in particular
Containers & Kubernetes monitoring. See the
llama.cpp server README (/metrics) and
vLLM — Documentation (metrics).
A cost-comparison method
This site quotes no prices — they change monthly and differ per contract. What stays valid is the method. Over a period (say a month), the cost of serving locally is
where is the hardware price amortised over , the average power draw in kW, the hours per month, the electricity price, the people time to run it, and rack or cloud-GPU rental. The hosted cost for the same traffic is
with , the monthly input and output tokens and , the provider’s per-token prices (see Cost, Latency & Throughput). Local serving breaks even when , but only if the hardware can actually carry the volume:
where is the achievable utilisation (traffic is not flat). To use it: measure goodput on your hardware with the harness on vLLM & SGLang, take your real token volumes from the gateway’s logs, plug in your own prices, and compare equal quality — a local model that needs more retries or more human correction is not cheaper. Privacy, residency and offline requirements often decide the question before cost does (see Running LLMs Locally).
For a managed alternative that keeps data in a chosen cloud region without running the servers yourself, see AI, Data and IoT Services Overview for Azure’s AI services.