Reach Your Local Ollama from a Cloud Server with Tailscale
When a cloud server, not your browser, calls the Ollama on your laptop or GPU machine, CORS has nothing to do with it, and OLLAMA_ORIGINS won’t help. Two other things stand in the way: the server can’t reach your machine, and Ollama listens only on localhost. Tailscale solves the first without opening a port to the internet. OLLAMA_HOST solves the second.
Browser or server: who sends the request?
My Ollama CORS post is about requests from a web page or a browser extension. CORS is a browser rule. The browser adds an Origin header to a cross-site request and decides, based on the answer, whether the page may read the response.
Ollama’s CORS check only acts on requests that carry an Origin header. A request without one passes it untouched. Server-side HTTP clients, such as Python’s httpx, the OpenAI SDK or curl, don’t send Origin unless you add it yourself. So when your backend calls Ollama, OLLAMA_ORIGINS doesn’t matter.
You can see it with curl against an Ollama that listens on its tailnet address:
curl -s -o /dev/null -w "%{http_code}\n" http://100.x.y.z:11434/api/version
# 200
curl -s -o /dev/null -w "%{http_code}\n" -H "Origin: https://app.example.com" http://100.x.y.z:11434/api/version
# 403
Same server, same request. The only difference is the header a browser would add.
Step 1: Put both machines on a tailnet
Tailscale gives every device on your tailnet a private address in the 100.x.y.z range and a name like mymac.tail1234.ts.net. Nothing changes on your router and nothing is opened to the internet: the tailnet address can be reached only from devices on the tailnet.
If the server is yours, install Tailscale on it and on the Ollama machine with the same account. That’s all.
If the server belongs to someone else’s tailnet, which is the case for a hosted app, share the machine instead. In Tailscale’s admin console, open Machines, choose Share from the machine’s menu, and copy the invite link. The other side accepts it, and your machine shows up in their tailnet. A shared machine can receive connections from that tailnet but can’t open connections into it. From their side, it’s reached by its full name, mymac.tail1234.ts.net; the short name won’t resolve there.
Step 2: Make Ollama listen on the tailnet address
Ollama binds to 127.0.0.1:11434 by default, so even with Tailscale up, nothing outside the machine can connect. Set OLLAMA_HOST to the machine’s tailnet address. Find it with:
tailscale ip -4
On Linux, with the systemd service from Ollama’s install script:
sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_HOST=100.x.y.z:11434"
sudo systemctl daemon-reload
sudo systemctl restart ollama
If Ollama starts before Tailscale has its address, the bind fails with bind: cannot assign requested address. The unit the install script writes has Restart=always and RestartSec=3, so it keeps trying until the address exists.
On a Mac with the Ollama app:
launchctl setenv OLLAMA_HOST "100.x.y.z:11434"
Then quit and reopen Ollama. Leave the app’s Expose Ollama to the network setting off: it sets OLLAMA_HOST to 0.0.0.0 and replaces yours.
One side effect: nothing listens on localhost anymore, so the ollama command in your terminal needs export OLLAMA_HOST=100.x.y.z:11434 too, and a local app that expects localhost:11434 needs the new address. If you need both, 0.0.0.0 listens on every interface, the tailnet included, but also on whatever Wi-Fi you’re on.
Binding to the tailnet address also avoids Ollama’s Host check. When Ollama listens only on loopback, it answers 403 to requests whose Host header isn’t a local name, which is what trips up reverse proxies and tunnels in front of localhost:11434. Bound to a non-loopback address, it skips that check.
Step 3: Point the server at it
From the server:
curl http://mymac.tail1234.ts.net:11434/api/version
curl http://mymac.tail1234.ts.net:11434/v1/models
If both answer, use the address as the base URL in your client:
from openai import OpenAI
client = OpenAI(base_url="http://mymac.tail1234.ts.net:11434/v1", api_key="ollama")
The traffic is encrypted by Tailscale, so plain http:// inside the tailnet is fine.
A note on security
- Ollama’s API has no authentication. Anything that can reach the port can run your models, and also pull and delete them.
- Bind to the tailnet address rather than
0.0.0.0unless the machine only ever sits on networks you trust. - A share gives the other tailnet a path to the machine, not just to port 11434. Tailscale’s access rules can narrow it down, and
autogroup:sharedstands for the users you’ve shared with.
How I ran into it
Catalyst runs in the cloud, and the bigger open models I use with it run on my own GPU machine. The requests to the model come from its server, a Python backend that calls the provider with httpx and sends no Origin header. So the CORS post wasn’t the one to follow: the server needed a route to the machine and an address to connect to, not an allowed origin.
Its Network page is the receiving end of Step 1. You choose Add Device and paste the Tailscale invite link for your machine (it starts with https://login.tailscale.com/admin/invite/), the server accepts the share on its own tailnet, and the device shows up with its tailnet name and address. Then you do Step 2 on the machine and point the Ollama service’s endpoint at http://<name>:11434/v1 instead of localhost.
One part does run in the browser: importing an Ollama service’s model list fetches /v1/models from the page. That’s a browser request, so the CORS rules apply to it. With a tailnet address, add the models by name instead.