The Mini Rack Saga - Part 4: The Self-Hosted AIML Stack
This is the one we’ve been building toward. In Parts 1 through 3 we built a physical rack, set up two Raspberry Pi 5s, and stood up a highly available RKE2 cluster on three Intel Mac minis with Tailscale handling all the connectivity. Now we’re putting that hardware to work.
In Part 4 we’re deploying a fully self-hosted AI stack on an M4 Pro Mac mini — Ollama running Gemma 4 natively, LiteLLM as the model proxy with per-user usage tracking, and Open WebUI as the frontend with Keycloak SSO. No cloud, no subscriptions, no data leaving your environment.
Let’s get into it.
Architecture Overview Link to heading
Before we dive in, here’s how everything fits together:
| Component | Where | Role |
|---|---|---|
| Ollama + Gemma 4 | M4 Pro Mac mini (bare metal) | Runs the model |
| LiteLLM | M4 Pro Mac mini (Docker) | Model proxy, per-user API keys, usage stats |
| Open WebUI | M4 Pro Mac mini (Docker) | Frontend, Keycloak SSO |
| Postgres | storage_pi | Database backend |
| Keycloak | RKE2 cluster (pre-existing) | SSO identity provider |
| Tailscale | M4 Pro Mac mini | Exposes services to the tailnet |
The flow looks like this:
User → Tailscale → Open WebUI → LiteLLM → Ollama (native)
↓ ↓
Keycloak Postgres (storage_pi)
Ollama runs natively on macOS, taking full advantage of the M4 Pro’s unified memory for fast local inference. LiteLLM and Open WebUI run as Docker containers on the same machine, talking to Ollama over localhost. Postgres lives on the storage_pi from Part 2 — we’re putting that dedicated database node to good use.
Both LiteLLM and Open WebUI authenticate via Keycloak SSO — no shared passwords, no local accounts, just clean identity management across the whole stack.
Step 1: Install Ollama and Pull Gemma 4 Link to heading
The M4 Pro Mac mini is an excellent inference machine. Apple Silicon’s unified memory architecture means the GPU and CPU share the same memory pool, so large models fit comfortably and run fast. Gemma 4 runs beautifully on it.
Install Ollama Link to heading
Head to ollama.com and download the Mac app, or install via the terminal:
curl -fsSL https://ollama.com/install.sh | sh
Pull Gemma 4 Link to heading
ollama pull gemma4:27b
Give it a few minutes to download. Once it’s done, do a quick sanity check:
ollama run gemma4:27b "Hello, are you there?"
You should get a response back. Ollama is up and running.
Verify the API Link to heading
Ollama exposes a local API on port 11434. By default it only listens on localhost, which is exactly what we want since LiteLLM will be talking to it from the same machine:
curl http://localhost:11434/api/tags
You should see Gemma 4 listed in the response.
Step 2: Set Up Tailscale on the Mac Mini Link to heading
Tailscale is what makes this whole setup accessible from anywhere on your tailnet without exposing anything to the public internet.
Install Tailscale Link to heading
Download the .pkg installer from pkgs.tailscale.com — this is the recommended path as it installs the macOS System Extension automatically. Run the installer, then launch Tailscale from your Applications folder and sign in.
Once connected, note down the Mac mini’s Tailscale IP — you’ll need it when configuring LiteLLM’s SSO redirect URI.
Step 3: Configure Keycloak Link to heading
This guide assumes Keycloak is already running. If you need to set it up, that’s a topic for a dedicated article.
Both LiteLLM and Open WebUI need their own Keycloak clients. Head to your Keycloak admin console and create two clients in your realm.
Client 1: LiteLLM Link to heading
- Go to Clients → Create client
- Set Client ID to
litellm - Set Client authentication to
On(confidential) - Under Valid redirect URIs add:
http://<your-mac-mini-tailscale-ip>:4000/sso/callback - Save, go to the Credentials tab and copy the Client Secret
Client 2: Open WebUI Link to heading
- Go to Clients → Create client
- Set Client ID to
open-webui - Set Client authentication to
On(confidential) - Under Valid redirect URIs add:
https://<your-mac-mini-tailscale-hostname>/oauth/oidc/callback - Save, go to the Credentials tab and copy the Client Secret
Note the client secrets for both — you’ll need them in the .env file next.
Step 4: Environment File Link to heading
Sensitive values live in a .env file alongside your docker-compose.yml. Never commit this to git.
Postgres prerequisite: LiteLLM and Open WebUI each need their own database on the storage Pi. Before continuing, make sure
litellmandopenwebuidatabases exist on your Postgres instance. If you set up the storage Pi in Part 2, you already have Postgres running — just create the two databases.
Create .env in your project directory:
# Postgres
POSTGRES_PASSWORD=<your-postgres-password>
# LiteLLM
LITELLM_MASTER_KEY=<your-litellm-master-key>
LITELLM_SALT_KEY=<your-litellm-salt-key>
LITELLM_KEYCLOAK_CLIENT_SECRET=<your-litellm-keycloak-client-secret>
# Open WebUI
WEBUI_SECRET_KEY=<your-webui-secret-key>
WEBUI_URL=https://<your-mac-mini-tailscale-hostname>
OAUTH_CLIENT_ID=open-webui
OAUTH_CLIENT_SECRET=<your-openwebui-keycloak-client-secret>
OPENID_PROVIDER_URL=https://<your-keycloak-tailscale-hostname>/realms/<your-realm>/.well-known/openid-configuration
Step 5: Docker Compose Link to heading
With the .env file in place, create docker-compose.yml in the same directory:
networks:
openwebui:
driver: bridge
volumes:
openwebui_data:
services:
litellm:
image: litellm/litellm-non_root:main-latest
container_name: litellm
restart: unless-stopped
command: ["--config", "/app/config.yaml", "--port", "4000"]
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@<storage-pi-ip>:5433/litellm
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
- LITELLM_SALT_KEY=${LITELLM_SALT_KEY}
- PROXY_ADMIN_ID=<your-keycloak-username>
- GENERIC_DEFAULT_USER_ROLE=internal_user
- GENERIC_CLIENT_ID=litellm
- GENERIC_CLIENT_SECRET=${LITELLM_KEYCLOAK_CLIENT_SECRET}
- GENERIC_AUTHORIZATION_ENDPOINT=https://<your-keycloak-tailscale-hostname>/realms/<your-realm>/protocol/openid-connect/auth
- GENERIC_TOKEN_ENDPOINT=https://<your-keycloak-tailscale-hostname>/realms/<your-realm>/protocol/openid-connect/token
- GENERIC_USERINFO_ENDPOINT=https://<your-keycloak-tailscale-hostname>/realms/<your-realm>/protocol/openid-connect/userinfo
- GENERIC_REDIRECT_URI=http://<your-mac-mini-tailscale-ip>:4000/sso/callback
volumes:
- ./litellm-config.yaml:/app/config.yaml:ro
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
networks:
- openwebui
openwebui:
image: openwebui/open-webui:0.9
container_name: openwebui
restart: unless-stopped
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@<storage-pi-ip>:5433/openwebui
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
- WEBUI_URL=${WEBUI_URL}
- ENABLE_OLLAMA_API=false
- OPENAI_API_BASE_URLS=http://litellm:4000/v1
- OPENAI_API_KEYS=${LITELLM_MASTER_KEY}
- ENABLE_LOGIN_FORM=false
- ENABLE_OAUTH_SIGNUP=true
- ENABLE_OAUTH_ROLE_MANAGEMENT=true
- OAUTH_ADMIN_ROLES=admin
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
- OAUTH_PROVIDER_NAME=Keycloak
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
- OPENID_PROVIDER_URL=${OPENID_PROVIDER_URL}
- OAUTH_SCOPES=openid email profile
volumes:
- openwebui_data:/app/backend/data
depends_on:
litellm:
condition: service_healthy
networks:
- openwebui
A few things worth calling out:
litellm-non_root— we’re using the rootless LiteLLM image, which is the more secure optionENABLE_LOGIN_FORM=false— Open WebUI has no local login form; Keycloak is the only way inENABLE_OAUTH_ROLE_MANAGEMENT=true— Keycloak roles map directly to Open WebUI roles; users with theadminrole in Keycloak get admin access in Open WebUI automaticallydepends_onwith health check — Open WebUI won’t start until LiteLLM is fully healthy, preventing startup race conditionsPROXY_ADMIN_ID— your Keycloak username, grants you admin access to the LiteLLM UI via SSO
LiteLLM Config File Link to heading
Create litellm-config.yaml in the same directory:
model_list:
- model_name: gemma4
litellm_params:
model: ollama/gemma4:27b
api_base: http://host.docker.internal:11434
general_settings:
store_model_in_db: true
The host.docker.internal hostname lets the LiteLLM container reach Ollama running natively on the Mac mini without any extra networking configuration.
Bring It Up Link to heading
docker compose up -d
Check everything is running:
docker compose ps
Both containers should show as running. LiteLLM will take a moment to pass its health check before Open WebUI starts.
Expose Services via Tailscale Serve Link to heading
With both containers up, use tailscale serve to put them on the tailnet with HTTPS and automatic TLS:
# Expose Open WebUI
tailscale serve --https=443 http://localhost:8080
# Expose LiteLLM
tailscale serve --https=4000 http://localhost:4000
Both services will now be accessible via your Mac mini’s MagicDNS hostname on your tailnet.
Step 6: Create LiteLLM API Keys Per User Link to heading
Once LiteLLM is up, head to http://<your-mac-mini-tailscale-ip>:4000 and sign in with Keycloak. Navigate to Virtual Keys and create a key per user. Each key:
- Routes to the same Ollama backend
- Tracks token usage independently
- Can have spend limits and rate limits applied
Open WebUI uses the master key to talk to LiteLLM on behalf of all users — the per-user keys are for direct API access or granular usage reporting.
Step 7: Verify the Stack Link to heading
Test Ollama directly Link to heading
curl http://localhost:11434/api/tags
Test LiteLLM Link to heading
curl http://localhost:4000/health
Test the full flow Link to heading
- Open Open WebUI at
https://<your-mac-mini-tailscale-hostname> - Click Sign in with Keycloak
- Log in and get redirected back to Open WebUI
- Start a conversation with Gemma 4
- Head to the LiteLLM UI and verify your request shows up in the usage dashboard
If all of that works — you’re done.
Wrapping Up Link to heading
Let’s take a moment to appreciate what we’ve built across this series:
- ✅ Part 1 — Physical rack build
- ✅ Part 2 — Raspberry Pi software setup, NVMe boot, Docker stacks
- ✅ Part 3 — HA RKE2 cluster on Mac minis, Tailscale Operator
- ✅ Part 4 — Self-hosted AIML stack: Ollama, LiteLLM, Open WebUI, Keycloak SSO
A full self-hosted AI stack, running on your own hardware, with no data leaving your environment. Per-user usage tracking. SSO via Keycloak across both LiteLLM and Open WebUI. Accessible from anywhere on your tailnet. That’s not a toy — that’s a production-grade homelab AI setup.
In Part 5 we put this stack to work as a coding agent — installing opencode-offline on a developer laptop and pointing it at LiteLLM over Tailscale for a fully offline AI coding experience. See you there.