docker run line without telling you which of the four possible arrangements you’re choosing, or why the answer changes completely depending on what silicon you own.The Decision That Comes First#
Every local AI tutorial opens with either “install Ollama” or “here’s a compose file,” and treats the choice as taste. It isn’t. It is determined almost entirely by one question:
Can the thing you’re about to run reach your GPU?
Containers get access to hardware only when the host explicitly hands it over. On Linux with an NVIDIA card there is a supported mechanism for that. On an Apple Silicon Mac there is not — Docker Desktop cannot pass the GPU through at all, because Apple’s virtualisation framework exposes no GPU interface to the VM that Docker runs inside.
That is not an Ollama bug and no flag fixes it. Put Ollama in a container on a Mac and it will run — silently, correctly, and entirely on the CPU.
| Host | Ollama in Docker? | Why |
|---|---|---|
| Linux + NVIDIA | Yes | NVIDIA Container Toolkit passes the device through |
| Linux + AMD | Yes | ROCm devices can be mapped in, with more friction |
| Apple Silicon | No — CPU only | No GPU passthrough exists on macOS |
| Windows + NVIDIA (WSL2) | Yes | GPU passthrough works through WSL2 |
| Any host, CPU-only | Sure | Nothing to pass through |
Recall from Part 2 that generation speed is bounded by memory bandwidth. A Mac’s unified memory has bandwidth in the hundreds of GB/s; its CPU cores reading through the same memory without Metal do not get you the same throughput. Containerising Ollama on a Mac doesn’t cost you a little performance. It costs you most of it.
So the rule that resolves every arrangement below:
Installing Ollama Natively#
On Linux:
| |
Worth knowing what that does before you pipe a script into a shell: it drops a binary in /usr/local/bin, creates a dedicated ollama system user, and installs and enables a systemd unit running as that user. It is a system service from the first second, started on boot, not something tied to your login session.
On macOS you download the application. Metal acceleration is automatic — no drivers, no configuration, nothing to enable.
Verify it is actually up, from the outside rather than the CLI:
| |
An empty models array is a success. You have a running server with nothing pulled yet. Then:
| |
ollama pull with a bare tag lets Ollama choose the quantisation on your behalf — usually Q4_K_M. Part 2 covered why that default is a reasonable one and how to work out whether a given file will actually fit in your VRAM before you spend the bandwidth downloading it.GPU Passthrough on Linux#
If you’re containerising Ollama on Linux with an NVIDIA card, this is the part that people skip and then spend an evening debugging.
First, prove the host can see the GPU. If this fails, nothing downstream can possibly work:
| |
No output means a driver problem, and the container toolkit will not rescue you. Fix the host first.
Then install the toolkit and point Docker at it:
| |
The middle command edits /etc/docker/daemon.json to register NVIDIA’s runtime. The restart is not optional — Docker reads that file at start.
Now prove a container can see the GPU, before involving Ollama at all:
| |
Same table as on the host means passthrough works. This two-step verification — host first, then a throwaway container — is worth doing every time, because it splits “my GPU isn’t working” into two questions with different answers.
Only then:
| |
no-cgroups = true in /etc/nvidia-container-runtime/config.toml. If you’ve deliberately gone rootless, budget time for this. If you don’t know whether you have, you haven’t.How the Two Containers Find Each Other#
This is where most people’s first attempt dies, and it always dies the same way: localhost inside a container means the container itself. Open WebUI looking for Ollama on http://localhost:11434 is looking inside its own network namespace, finding nothing, and showing you an empty model dropdown.
There are three arrangements, and they differ only in what address the frontend uses.
graph TD
subgraph A["Both native"]
A1["Open WebUI"] -->|"localhost:11434"| A2["Ollama"]
end
subgraph B["Frontend in Docker, runtime native"]
B1["Open WebUI container"] -->|"host.docker.internal:11434"| B2["Ollama on host"]
end
subgraph C["Both in Compose"]
C1["open-webui service"] -->|"http://ollama:11434"| C2["ollama service"]
end
Arrangement B — frontend containerised, Ollama native. This is the right answer on Apple Silicon, and a good one on Linux too: Ollama gets unmediated GPU access, and the web app stays disposable.
| |
The --add-host flag is doing the real work. On Docker Desktop host.docker.internal already resolves; on Linux it does not exist until you create it, and host-gateway is the magic value that resolves to the host from inside the container.
Arrangement C — both in Compose. Containers on a user-defined network resolve each other by service name. http://ollama:11434 works because Docker runs a DNS resolver on that network, and ollama is the service’s name in the compose file. No IP addresses, no --add-host, no host networking.
A Compose File Worth Keeping#
| |
Four things in there are deliberate:
Ollama publishes no ports. There is no ports: block on the runtime. It is reachable by open-webui over the internal network and by nothing else. Given that Ollama’s API has no authentication, not publishing it is the single most valuable line in this file — or rather, the most valuable line that isn’t there.
OLLAMA_BASE_URL uses the service name. This is the arrangement-C wiring from above, stated explicitly rather than relying on a default.
depends_on controls start order, not readiness. It waits for the container to start, not for the server inside it to answer. Open WebUI starting slightly before Ollama is ready is harmless — it just means the model list is briefly empty. Don’t read more into the flag than it does.
Both volumes are named. Which brings us to the part people only think about once.
Where Your Data Actually Lives#
| What | Native path | Container path |
|---|---|---|
| Models (systemd install) | /usr/share/ollama/.ollama/models | /root/.ollama |
| Models (manual/user install) | ~/.ollama/models | /root/.ollama |
| Models (macOS) | ~/.ollama/models | — |
| Open WebUI database | — | /app/backend/data |
The Linux model path trips people up because it depends on which user runs the server. The install script’s systemd unit runs as the ollama user, so models land in /usr/share/ollama/.ollama/models. Run the binary yourself and they land in ~/.ollama/models instead. Same software, two locations, and “where did my models go” is usually this.
Point OLLAMA_MODELS at a bigger disk if you need to — on Linux, make sure the ollama user can actually write there:
| |
These two volumes are not equally precious, and most people guard the wrong one.
ollama:/root/.ollama holds model weights. It might be 50 GB. Losing it costs you a download.
open-webui:/app/backend/data holds every conversation, user account, setting and uploaded document. It might be 50 MB. Losing it costs you everything you cannot get back.
Back up the small one.
That asymmetry is Part 3’s argument showing up in your filesystem. The model is stateless and replaceable; the state lives in the frontend. Your backup strategy should follow the state, not the gigabytes. If you already run a homelab with real backups, this volume belongs in that rotation.
The Binding Question#
Part 3 deferred this deliberately, so here it is.
By default Ollama binds to 127.0.0.1:11434 — reachable only from the machine it runs on. To change that under systemd:
| |
| |
| |
Now think hard about whether you want this.
0.0.0.0 means every interface — Wi-Fi, Ethernet, VPN, everything. And Ollama has no authentication of any kind. There is no API key, no user model, no rate limit. Anything that can open a TCP connection to that port can list your models, run inference on your GPU, load a 40 GB model to exhaust your VRAM, or pull arbitrary models onto your disk.
The safe options, in the order I’d reach for them:
Private network. Put the machine on a tailnet and bind to that interface only. I’ve written up reaching Ollama across cities over Tailscale — the runtime never touches a public interface, and the network layer does the authentication.
SSH tunnel, for a one-off from a machine you can already log into. Port forwarding over SSH gets you a local port without binding to anything.
Expose the frontend instead. Open WebUI does have accounts and sessions. If you want browser access from your phone, put a reverse proxy and TLS in front of Open WebUI on port 3000 and leave Ollama on localhost. You are then exposing the layer that was built to be exposed.
Firewall the port if you must bind widely — scope the rule to a single interface or subnet rather than trusting the bind address alone. My iptables introduction covers the mechanics.
The pattern here is the one from Part 1: each layer does one job. Authentication is not the runtime’s job, so don’t put the runtime where it needs to do it.
When It Doesn’t Work#
Almost every installation failure maps to a layer, exactly as promised in Part 1.
| Symptom | Almost always |
|---|---|
| Open WebUI loads, model dropdown empty | Frontend can’t reach the runtime — localhost vs host.docker.internal vs service name |
connection refused on 11434 | Ollama not running, or bound to localhost while you’re calling from elsewhere |
| Works on host, fails in container | Missing --add-host=host.docker.internal:host-gateway on Linux |
| Model runs but the GPU is idle | Passthrough not configured — re-run the two-step nvidia-smi check |
| Generation is inexplicably slow on a Mac | Ollama is in Docker. It is on the CPU. Move it to the host |
| Models vanished after reinstalling | Different user, different .ollama directory |
Conversations gone after docker compose down -v | -v deletes named volumes. This is the one that hurts |
That last row deserves its own warning. docker compose down stops containers; docker compose down -v also removes the volumes, and the flag is one keystroke away from routine.
| |
What You’ve Learned#
- Native versus Docker is decided by GPU access, not preference. Apple Silicon cannot pass a GPU into a container at all, so Ollama in Docker on a Mac is CPU-only and dramatically slower.
- The runtime goes where the GPU is; the frontend can live anywhere.
- On Linux, verify GPU passthrough in two steps —
nvidia-smion the host, then in a throwaway container — before blaming Ollama. localhostinside a container is the container. Usehost.docker.internalwith--add-hoston Linux, or a Compose service name.- Don’t publish Ollama’s port. It has no authentication, so the network boundary has to come from somewhere else: a tailnet, an SSH tunnel, or a reverse proxy in front of the frontend rather than the runtime.
- Model storage on Linux depends on which user runs the server, which explains most “my models disappeared” reports.
- The 50 MB Open WebUI volume matters more than the 50 GB Ollama one. Weights are re-downloadable; conversations are not.
Next in the Series#
Part 5: Where to Go Next. The stack is running, so what do you actually build on it? Embeddings and what a vector really is, how RAG works and the specific ways it goes wrong, why “chat with your documents” is a retrieval problem rather than an AI one, and where agents fit — plus an honest look at which of these is worth your time on local hardware.
