CPU-Only Ollama Cluster on Proxmox LXC: 3-Node Setup with Load Balancing

Run local LLMs across three Proxmox LXC containers with Ollama, expose them via Open WebUI's built-in load balancer, and avoid the Docker-in-LXC pitfalls that cost me two sessions to fix.

Running local LLMs on commodity hardware is genuinely practical in 2026 — if you’re willing to accept CPU-speed inference (~2–12 tok/s) in exchange for zero cloud cost and complete privacy. This post covers deploying Ollama across three Proxmox LXC containers, wiring them to Open WebUI for load-balanced access, and the specific gotchas I hit along the way.


Architecture

Browser / App:8080Open WebUI (CT 103)10.25.144.69:8080 · OLLAMA_BASE_URLS round-robin <rect x="60" y="168" width="170" height="90" rx="8" fill="#1f2937" stroke="#27272a" stroke-width="1.5"/> <text x="145" y="186" text-anchor="middle" fill="#f4f4f5" font-size="10" font-family="monospace" font-weight="bold">CT 100 / pvelab01</text> <text x="145" y="202" text-anchor="middle" fill="#14b8a6" font-size="9" font-family="monospace">10.25.144.65:11434</text> <text x="145" y="220" text-anchor="middle" fill="#a1a1aa" font-size="9" font-family="monospace">qwen2.5-coder:14b / llama3.2</text> <text x="145" y="238" text-anchor="middle" fill="#71717a" font-size="9" font-family="monospace">~2 tok/s · Ryzen 5 2400G</text> <rect x="270" y="168" width="170" height="90" rx="8" fill="#1f2937" stroke="#27272a" stroke-width="1.5"/> <text x="355" y="186" text-anchor="middle" fill="#f4f4f5" font-size="10" font-family="monospace" font-weight="bold">CT 101 / pvelab02</text> <text x="355" y="202" text-anchor="middle" fill="#14b8a6" font-size="9" font-family="monospace">10.25.144.66:11434</text> <text x="355" y="220" text-anchor="middle" fill="#a1a1aa" font-size="9" font-family="monospace">qwen2.5-coder:14b / llama3.2</text> <text x="355" y="238" text-anchor="middle" fill="#71717a" font-size="9" font-family="monospace">~2 tok/s · Ryzen 5 2400G</text> <rect x="480" y="168" width="170" height="90" rx="8" fill="#1f2937" stroke="#27272a" stroke-width="1.5"/> <text x="565" y="186" text-anchor="middle" fill="#f4f4f5" font-size="10" font-family="monospace" font-weight="bold">CT 102 / pvelab03</text> <text x="565" y="202" text-anchor="middle" fill="#14b8a6" font-size="9" font-family="monospace">10.25.144.67:11434</text> <text x="565" y="220" text-anchor="middle" fill="#a1a1aa" font-size="9" font-family="monospace">llama3.2</text> <text x="565" y="238" text-anchor="middle" fill="#71717a" font-size="9" font-family="monospace">~3 tok/s · Ryzen 5 2400G</text>

Performance expectations

Before diving in: CPU inference is slow compared to a GPU. Here’s what to expect on Ryzen 5 2400G hardware:

Model Size Tokens/sec Use case
llama3.2:3b ~2 GB 8–12 tok/s Quick queries, fast chat
qwen2.5-coder:14b ~9 GB 2–3 tok/s Code gen, complex reasoning

These speeds are per-node. Open WebUI’s round-robin distributes concurrent requests across all three nodes — parallel users each get a dedicated node.


Task 1: Create three Ubuntu 24.04 LXC containers

1Download the Ubuntu 24.04 LXC template3 min

In the Proxmox web UI: select any node → local storage → CT Templates → Templates. Search for ubuntu-24.04 and click Download.

2Create CT 100 on pvelab015 min

Datacenter → pvelab01 → Create CT. Settings:

Field Value
VMID 100
Hostname ollama-node1
Template ubuntu-24.04
Disk 20 GB on local-lvm
Cores 4
RAM 14336 MB (14 GB)
Network DHCP → then set static to 10.25.144.65
Unprivileged Yes
Tip

Allocate 14 GB RAM per node. qwen2.5-coder:14b loads ~9 GB into memory; you need headroom for the OS and concurrent requests. Nodes with only 8 GB host the smaller llama3.2 model only.

3Repeat for CT 101 (pvelab02) and CT 102 (pvelab03)10 min

Create two more containers with the same spec. Assign:

  • CT 101: pvelab02, IP 10.25.144.66, RAM 6144 MB (8 GB node — llama3.2 only)
  • CT 102: pvelab03, IP 10.25.144.67, RAM 4096 MB (8 GB node — llama3.2 only)

Start all three containers.


Task 2: Install Ollama on each node

Run these steps inside each container. Access via pct exec <vmid> -- bash from the Proxmox host.

1Install Ollama via the official installer5 min
pct exec 100 -- bash (repeat for 101, 102)

curl -fsSL https://ollama.com/install.sh | sh
systemctl enable --now ollama
2Configure Ollama to listen on all interfaces3 min

By default, Ollama binds to localhost:11434. Open WebUI needs to reach it over the container network, so override the bind address:

Configure Ollama systemd override

mkdir -p /etc/systemd/system/ollama.service.d
cat > /etc/systemd/system/ollama.service.d/override.conf {'<<'} 'EOF'
[Service]
Environment="OLLAMA_HOST=0.0.0.0"
Environment="OLLAMA_NUM_THREADS=8"
EOF
systemctl daemon-reload && systemctl restart ollama
Note

OLLAMA_NUM_THREADS=8 pins Ollama to all 8 logical cores of the 2400G. Without this, Ollama may auto-detect fewer threads and leave performance on the table.

3Pull models on each node20–40 min depending on internet speed

On nodes with 14+ GB RAM (pvelab01):

CT 100 — pull both models

ollama pull llama3.2
ollama pull qwen2.5-coder:14b

On 8 GB nodes (pvelab02, pvelab03):

CT 101 / 102 — pull llama3.2 only

ollama pull llama3.2
4Verify all three nodes are responding2 min
Test from pvelab04 host

curl http://10.25.144.65:11434/api/tags
curl http://10.25.144.66:11434/api/tags
curl http://10.25.144.67:11434/api/tags

Each should return a JSON object listing installed models. If any fails, check systemctl status ollama inside that container.


Task 3: Deploy Open WebUI natively (not Docker)

Do not use Docker inside LXC

Docker inside an unprivileged LXC container requires nesting=1 and causes iptables/nftables rule conflicts. The Docker container also sets a FORWARD drop policy that blocks all traffic from your Ollama containers to the gateway. This is exactly what happened in my initial setup — it cost two sessions to diagnose and clean up. Use the native Python install instead.

1Create CT 103 for Open WebUI on pvelab035 min
Field Value
VMID 103
Hostname open-webui
IP 10.25.144.69
RAM 2048 MB
Cores 2
Unprivileged Yes
2Install Open WebUI in a Python venv10 min
Inside CT 103

apt-get update && apt-get install -y python3-venv python3-pip
useradd -m -s /bin/bash openwebui
su - openwebui -c "python3 -m venv /home/openwebui/venv"
su - openwebui -c "/home/openwebui/venv/bin/pip install open-webui"
3Create the systemd service with OLLAMA_BASE_URLS5 min
Create /etc/systemd/system/open-webui.service

cat > /etc/systemd/system/open-webui.service {'<<'} 'EOF'
[Unit]
Description=Open WebUI
After=network.target

[Service]
User=openwebui
WorkingDirectory=/home/openwebui
Environment="OLLAMA_BASE_URLS=http://10.25.144.65:11434;http://10.25.144.66:11434;http://10.25.144.67:11434"
Environment="DATA_DIR=/home/openwebui/data"
ExecStart=/home/openwebui/venv/bin/open-webui serve
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl enable --now open-webui

The semicolon-separated OLLAMA_BASE_URLS gives Open WebUI its round-robin load balancer. Requests are distributed across all three Ollama nodes automatically.

4Verify load balancing in the Open WebUI interface2 min

Open http://10.25.144.69:8080 in your browser. After creating an admin account, go to Admin → Settings → Connections. You should see all three Ollama endpoints listed with green status indicators.

http://10.25.144.69:8080/admin/settingsOllama API Connections <text x="40" y="95" fill="#a1a1aa" font-size="10" font-family="monospace">http://10.25.144.65:11434</text> <circle cx="555" cy="90" r="6" fill="#22c55e"/> <text x="40" y="115" fill="#a1a1aa" font-size="10" font-family="monospace">http://10.25.144.66:11434</text> <circle cx="555" cy="110" r="6" fill="#22c55e"/> <text x="40" y="135" fill="#a1a1aa" font-size="10" font-family="monospace">http://10.25.144.67:11434</text> <circle cx="555" cy="130" r="6" fill="#22c55e"/>

Routing policy: when to use local vs. cloud

With three local nodes running, the question becomes: when to use local inference vs. Claude/GPT?

Model routing decisionUse local (Ollama)<text x="36" y="82" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Boilerplate & templates</text><text x="36" y="100" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Config file generation</text><text x="36" y="118" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Repetitive code tasks</text><text x="36" y="136" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Private/sensitive data</text><text x="36" y="154" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Batch offline processing</text>Use cloud (Claude / GPT)<text x="366" y="82" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Multi-file architecture decisions</text><text x="366" y="100" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Complex debugging</text><text x="366" y="118" fill="#a1a1aa" font-size="10" font-family="monospace">✓ High-stakes code review</text><text x="366" y="136" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Long multi-turn reasoning</text><text x="366" y="154" fill="#a1a1aa" font-size="10" font-family="monospace">✓ Novel problem solving</text>

The tl;dr: local inference is free and private; use it for everything that doesn’t require frontier-model reasoning quality.


Service management reference

Managing Ollama containers from any Proxmox host

# Check model list on all three nodes
for ct in 100 101 102; do
echo "=== CT $ct ==="; pct exec $ct -- ollama list
done

# Restart Ollama on a specific node
pct exec 100 -- systemctl restart ollama

# Watch inference logs in real time
pct exec 100 -- journalctl -u ollama -f

# Test a quick generation (expect ~2 tok/s)
curl http://10.25.144.65:11434/api/generate \
-d '{"model":"llama3.2","prompt":"Hello world","stream":false}'

Next in this series: wiring up Hermes Agent — a fully autonomous AI agent running on the cluster with Proxmox read access, web search, and automated weekly reports.