Executive Summary
127.0.0.1:11435 on the VPS, SSH carries that traffic back to 127.0.0.1:11434 on the Mac, and the answer returns along the same path. There is no public Ollama URL, no home-router port forwarding, and no new inbound firewall rule for port 11435.Cloud GPU servers are powerful, but they are not always necessary. For many retrieval-augmented generation systems, the public web server does not need to run the model. It needs to serve the browser app, secure the domain, manage the database, process documents, search the knowledge base, and call an AI endpoint.
At JAMD Technologies, we built a practical hybrid pattern: run the web app on an inexpensive VPS, run Ollama on a local Apple Silicon Mac, and connect the two with a private SSH reverse tunnel.
The result is a low-cost architecture with a useful upgrade path. As Mac Silicon gets faster, the local inference option improves without changing the public server design.
Who This Guide Is For
This guide is for a developer or small technical team with a normal Linux VPS or co-located web server, an Apple Silicon Mac, a server-side application that can call a configurable model endpoint, and SSH access from the Mac to the server.
The examples use Nginx, PHP, MariaDB, macOS, and Ollama, but the same network pattern works with Node.js, Python, Ruby, Go, or another server-side stack. The web server does not need a GPU, CUDA, or AI-specific hosting.
Why This Architecture Works
The expensive part of local AI is usually the model hardware. A cloud GPU server can cost far more than a normal VPS. But if you already have a powerful MacBook Pro, Mac Studio, or Mac mini with Apple Silicon, that machine may already have excellent local inference capability.
The VPS Handles
- Public HTTPS traffic
- Nginx, PHP, and MariaDB
- RAG search and chunk retrieval
- SSL certificates and scheduled jobs
The Mac Handles
- Ollama model inference
- Apple Silicon local acceleration
- Private local model testing
- Future upgrades as Macs get faster
The VPS does not need a GPU. It can be a simple Linux server running normal web infrastructure. Meanwhile, the Mac uses Ollama to generate answers locally.
The Components and Their Exposure
| Component | Job | Publicly reachable? |
|---|---|---|
| Browser | Sends a question and renders the answer | The user’s device |
| VPS web edge | Terminates HTTPS on ports 443/80 | Yes |
| VPS application | Authenticates, retrieves context, calls the model | Through the web edge only |
| VPS tunnel listener | Presents the Mac at 127.0.0.1:11435 | No—loopback only |
| Mac SSH client | Creates and maintains the reverse tunnel | Outbound connection only |
| Mac Ollama | Runs inference at 127.0.0.1:11434 | No—loopback only |
HTTPS, SSH, and Ollama: Three Different Roles
People sometimes describe the whole design as an “SSL tunnel,” but the distinction matters: HTTPS/TLS protects browser traffic to the application, while SSH protects and authenticates the private Mac-to-VPS model bridge.
- HTTPS/SSL protects browser traffic to the web app.
- SSH protects the private tunnel between the Mac and VPS.
- Local HTTP reaches Ollama only through loopback addresses.
- The VPS sees the remote Mac as a local-only service.
The complete path is encrypted on the public network even though the two Ollama URLs begin with http://. Those URLs are not public network hops; they refer to local processes on their respective machines.
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R 127.0.0.1:11435:127.0.0.1:11434 \
helpnow-ollama-vps
127.0.0.1 on both sides. HelpNow can reach Ollama from the VPS process, but the public internet cannot call the Ollama port directly.
What Happens When a User Asks a Question
- The browser sends
POST /api/askto the VPS over HTTPS. - Nginx terminates TLS and passes the authenticated request to the application.
- The application validates the request and, for RAG, retrieves relevant knowledge-base passages.
- The application posts the completed prompt to
http://127.0.0.1:11435/api/chat. - The VPS kernel accepts that connection only on loopback. SSH maps it to the reverse-forwarding channel created earlier by the Mac.
- The encrypted channel carries the bytes over the Mac’s already-established outbound SSH session.
- The Mac SSH client connects locally to
127.0.0.1:11434, where Ollama generates the answer. - The answer returns through the same SSH channel, application response, HTTPS connection, and browser request.
The browser never receives ports 11434 or 11435, and browser JavaScript never calls Ollama directly. The public application remains responsible for authentication, authorization, rate limiting, prompt construction, and presentation.
Step-by-Step Setup
Work from the inside out: prove Ollama works locally, prove SSH works, create the tunnel, prove the VPS loopback endpoint works, and only then connect the application.
ai-vps.example.com; dedicated user ollama-tunnel; VPS-local port 127.0.0.1:11435; Mac-local Ollama 127.0.0.1:11434; model llama3.2:latest.llama3.2:latest, start Ollama, and confirm http://127.0.0.1:11434/api/tags returns the local model list.GatewayPorts no so the tunnel stays private.http://127.0.0.1:11435/api/tags from the VPS and confirm it returns the Mac Ollama models.OLLAMA_BASE_URL to http://127.0.0.1:11435 and model to llama3.2:latest.1. Install and Test Ollama on the Mac
Install Ollama from the official macOS download, launch it once, and start with a model that fits comfortably in the Mac’s unified memory.
ollama --version
ollama pull llama3.2:latest
ollama list
curl -fsS http://127.0.0.1:11434/api/tags
Make one local generation request before adding the tunnel:
curl -fsS http://127.0.0.1:11434/api/chat \
-H 'Content-Type: application/json' \
-d '{
"model": "llama3.2:latest",
"stream": false,
"messages": [{"role":"user","content":"Reply with exactly: local Ollama works"}]
}'
If this request fails, fix Ollama first. A tunnel cannot repair a local model-server problem.
2. Give the Tunnel Its Own SSH Key
A dedicated key and non-root VPS account make revocation and auditing cleaner than reusing an administrator key.
ssh-keygen -t ed25519 \
-f ~/.ssh/ollama_tunnel_ed25519 \
-C "ollama-reverse-tunnel"
ssh-copy-id \
-i ~/.ssh/ollama_tunnel_ed25519.pub \
ollama-tunnel@ai-vps.example.com
If ssh-copy-id is unavailable, copy the public key’s single line into the VPS user’s ~/.ssh/authorized_keys using your normal administrative process. Never copy the private key.
Add a named entry to ~/.ssh/config on the Mac:
Host helpnow-ollama-vps
HostName ai-vps.example.com
User ollama-tunnel
IdentityFile ~/.ssh/ollama_tunnel_ed25519
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config ~/.ssh/ollama_tunnel_ed25519
chmod 644 ~/.ssh/ollama_tunnel_ed25519.pub
ssh helpnow-ollama-vps 'printf "SSH works\n"'
3. Allow Only the Forwarding the Tunnel Needs
Check the VPS’s effective OpenSSH settings:
sudo sshd -T | grep -E 'allowtcpforwarding|gatewayports'
A security-focused drop-in at /etc/ssh/sshd_config.d/ollama-tunnel.conf can constrain the dedicated user:
Match User ollama-tunnel
AllowTcpForwarding remote
GatewayPorts no
PermitListen 127.0.0.1:11435
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
sudo sshd -t
sudo systemctl reload ssh
sshd. Validate with sshd -t and keep a second administrative session open while changing SSH configuration.4. Start and Inspect the Temporary Tunnel
ssh -NT \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R 127.0.0.1:11435:127.0.0.1:11434 \
helpnow-ollama-vps
-Nruns no remote command; the session exists only for forwarding.-Rcreates the VPS listener and maps it back to the Mac.ExitOnForwardFailurefails immediately if port 11435 cannot be bound.- Keepalives identify a dead connection instead of leaving a stale-looking tunnel.
0.0.0.0. Changing the remote bind to -R 0.0.0.0:11435:... asks SSH to expose the listener on public interfaces and changes the security model.5. Test From the VPS Side
ssh helpnow-ollama-vps \
"curl -fsS http://127.0.0.1:11435/api/tags"
ssh helpnow-ollama-vps \
"ss -lnt | grep ':11435'"
The listener must show 127.0.0.1:11435—never 0.0.0.0:11435, [::]:11435, or the server’s public IP.
Reusable Helper Scripts
After the manual command works, wrap it in a script that fails early when local Ollama is unavailable and preserves the two localhost bindings.
Start the Reverse Tunnel
#!/usr/bin/env bash
set -euo pipefail
SERVER="${1:-helpnow-ollama-vps}"
REMOTE_PORT="${REMOTE_PORT:-11435}"
LOCAL_PORT="${LOCAL_PORT:-11434}"
REMOTE_BIND="${REMOTE_BIND:-127.0.0.1}"
LOCAL_BIND="${LOCAL_BIND:-127.0.0.1}"
printf 'Checking local Ollama at http://%s:%s ...\n' "$LOCAL_BIND" "$LOCAL_PORT"
if ! curl -fsS "http://${LOCAL_BIND}:${LOCAL_PORT}/api/tags" >/dev/null; then
printf 'Ollama is not responding locally. Start Ollama, then run this again.\n' >&2
exit 1
fi
exec ssh \
-N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R "${REMOTE_BIND}:${REMOTE_PORT}:${LOCAL_BIND}:${LOCAL_PORT}" \
"$SERVER"
chmod +x start-ollama-reverse-tunnel.sh
./start-ollama-reverse-tunnel.sh helpnow-ollama-vps
Check the Tunnel from the VPS
#!/usr/bin/env bash
set -euo pipefail
SERVER="${1:-helpnow-ollama-vps}"
OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11435}"
ssh "$SERVER" "curl -fsS '${OLLAMA_URL}/api/tags'"
Install as a Mac Background Service
#!/usr/bin/env bash
set -euo pipefail
SERVER="${1:-helpnow-ollama-vps}"
LABEL="${LABEL:-com.jamd.ollama-tunnel}"
REMOTE_PORT="${REMOTE_PORT:-11435}"
LOCAL_PORT="${LOCAL_PORT:-11434}"
REMOTE_BIND="${REMOTE_BIND:-127.0.0.1}"
LOCAL_BIND="${LOCAL_BIND:-127.0.0.1}"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SUPPORT_DIR="${HOME}/Library/Application Support/JAMD"
RUNNER_PATH="${SUPPORT_DIR}/start-ollama-reverse-tunnel.sh"
PLIST_DIR="${HOME}/Library/LaunchAgents"
PLIST_PATH="${PLIST_DIR}/${LABEL}.plist"
LOG_DIR="${HOME}/Library/Logs"
USER_ID="$(id -u)"
mkdir -p "$SUPPORT_DIR" "$PLIST_DIR" "$LOG_DIR"
cp "${PROJECT_DIR}/bin/start-ollama-reverse-tunnel.sh" "$RUNNER_PATH"
chmod 755 "$RUNNER_PATH"
xattr -d com.apple.provenance "$RUNNER_PATH" >/dev/null 2>&1 || true
xattr -d com.apple.quarantine "$RUNNER_PATH" >/dev/null 2>&1 || true
cat > "$PLIST_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${RUNNER_PATH}</string>
<string>${SERVER}</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>30</integer>
<key>StandardOutPath</key><string>${LOG_DIR}/JAMDOllamaTunnel.log</string>
<key>StandardErrorPath</key><string>${LOG_DIR}/JAMDOllamaTunnel.err.log</string>
</dict>
</plist>
PLIST
launchctl bootout "gui/${USER_ID}" "$PLIST_PATH" >/dev/null 2>&1 || true
launchctl bootstrap "gui/${USER_ID}" "$PLIST_PATH"
launchctl kickstart -k "gui/${USER_ID}/${LABEL}"
After installation, verify the agent and inspect its logs:
launchctl print gui/$(id -u)/com.jamd.ollama-tunnel
tail -f ~/Library/Logs/JAMDOllamaTunnel.err.log
The unattended key must be usable after macOS login without a terminal prompt. Test the exact SSH host alias after a reboot before relying on this path.
Connect the Web Application
Keep the tunnel URL in server-side configuration. Never expose it as a browser setting or ask JavaScript to call Ollama directly.
OLLAMA_TUNNEL_NAME=Mac reverse tunnel
OLLAMA_BASE_URL=http://127.0.0.1:11435
OLLAMA_MODEL=llama3.2:latest
OLLAMA_MAX_TOKENS=900
The application should allow-list the localhost URL, authenticate the browser user, authorize access to any retrieved documents, validate prompt size, and rate-limit requests before calling the model.
Minimal Server-Side PHP Example
<?php
$question = trim((string) ($_POST['question'] ?? ''));
if ($question === '' || mb_strlen($question) > 4000) {
http_response_code(422);
exit;
}
// Authenticate and rate-limit the user before this point.
$payload = json_encode([
'model' => getenv('OLLAMA_MODEL') ?: 'llama3.2:latest',
'stream' => false,
'messages' => [
['role' => 'system', 'content' => 'Answer clearly and concisely.'],
['role' => 'user', 'content' => $question],
],
], JSON_THROW_ON_ERROR);
$ch = curl_init('http://127.0.0.1:11435/api/chat');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 120,
]);
$raw = curl_exec($ch);
if ($raw === false) {
http_response_code(503);
echo json_encode(['error' => 'Local model is temporarily unavailable']);
exit;
}
$result = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
header('Content-Type: application/json');
echo json_encode(['answer' => $result['message']['content'] ?? '']);
The Browser Calls Your HTTPS Endpoint
const response = await fetch('/api/ask', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({question: userQuestion})
});
const result = await response.json();
answerElement.textContent = result.answer;
The public application remains the control plane. It owns authentication, authorization, rate limits, input limits, RAG retrieval, audit policy, timeouts, and presentation. Ollama performs inference only for the trusted local caller.
503 when the Mac is unavailable, bound connection and response timeouts, limit concurrent generations, avoid logging confidential prompts by default, and use a cloud fallback only when policy permits that content to leave the private path.Validate Every Layer
Test from the inside out so each failure has a small search area:
- Mac process:
ollama list - Mac API:
curl -fsS http://127.0.0.1:11434/api/tags - SSH authentication:
ssh helpnow-ollama-vps 'true' - VPS listener:
ssh helpnow-ollama-vps "ss -lnt | grep ':11435'" - Tunnel API: query
http://127.0.0.1:11435/api/tagsfrom the VPS. - Generation: run one
/api/chatrequest from the VPS. - Application: ask a browser question and verify authorization, answer, sources, and timing.
- Failure path: stop the tunnel and verify a fast, safe
503.
Finally, test the negative case from a third machine:
nc -vz ai-vps.example.com 11435
That public connection should fail. If it succeeds, stop the tunnel and correct the listener or firewall before proceeding.
Why No New Firewall Rule Is Needed
The Mac initiates a normal outbound SSH connection. Home routers, corporate edge firewalls, and stateful host firewalls ordinarily allow return traffic for an established outbound connection. SSH carries forwarded model requests as logical channels inside that one authenticated, encrypted session.
On the VPS, SSH creates 127.0.0.1:11435. Loopback traffic never arrives from a physical network interface, so there is no reason to open 11435 in UFW, firewalld, a cloud security group, or a provider firewall.
| Port | Direction | Purpose | Public? |
|---|---|---|---|
| 443 | Inbound to VPS | Browser HTTPS | Yes |
| 80 | Inbound to VPS | Optional HTTPS redirect / certificate validation | Usually |
| 22 | Inbound to VPS | Existing SSH from Mac and administrators | Yes, preferably restricted |
| 11435 | VPS loopback only | Reverse-tunnel listener | No |
| 11434 | Mac loopback only | Ollama API | No |
You do not configure port forwarding on the Mac’s router, expose the Mac’s IP address, or create a public DNS record for Ollama. Opening 11434 or 11435 to 0.0.0.0/0 describes a different—and substantially riskier—architecture.
Benchmarks from Our HelpNow Test
We benchmarked this through the real HelpNow application path using the Starlink knowledge base. The timing included RAG search, chunk selection, prompt construction, provider request, and model response time.
| Provider | Model | RAG | AI | Total | Result |
|---|---|---|---|---|---|
| Ollama tunnel | llama3.2:latest | 0.02s | 3.64s | 3.66s | OK |
| Anthropic | claude-haiku-4-5-20251001 | 0.02s | 3.51s | 3.53s | OK |
| Anthropic | claude-sonnet-4-5-20250929 | 0.03s | 5.35s | 5.38s | OK |
| Ollama tunnel | qwen3:8b | 0.02s | 10.00s | 10.02s | OK |
| Ollama tunnel | gemma3:12b | 0.02s | 10.54s | 10.56s | OK |
| Ollama tunnel | qwen3-vl:latest | 0.06s | 18.39s | 18.45s | OK |
| Ollama tunnel | llama3.3:70b | 0.03s | 42.97s | 43.00s | OK |
| OpenAI | gpt-5.6-luna | – | – | – | Skipped, API key needed correction |
llama3.2:latest landed within the same practical response range as Claude Haiku 4.5 for this support-style prompt. Larger local models worked, but they were slower than ideal for normal live chat.
Cost Logic and Upgrade Path
A normal VPS is inexpensive because it does not include high-end GPU resources. With this tunnel pattern, that is fine: the VPS does not need GPU capability at all. It handles public web operations while the Mac handles local inference.
- Start with cloud frontier models for reliability.
- Add the Mac tunnel for local/private model testing.
- Benchmark local models against cloud models inside the same app.
- Route selected workloads to local models when speed and quality are good enough.
- Upgrade the Mac or local model over time.
- Keep cloud fallback for uptime and peak workloads.
Security: What This Design Protects
The design is secure because it combines encryption with deliberate network non-exposure. Encryption alone is not enough; the loopback bindings keep Ollama from becoming a public service.
1. There Is No Internet Route to Ollama
Ollama listens at 127.0.0.1:11434 on the Mac. The VPS-side forward listens at 127.0.0.1:11435. A loopback address is reachable only by processes on that machine. Internet scanners cannot route packets to either address.
2. Public-Network Traffic Is Encrypted and Authenticated
The browser leg uses HTTPS/TLS. The Mac-to-VPS leg uses SSH. SSH verifies the VPS host key, authenticates the Mac with its dedicated key, and encrypts the prompt and model response in transit.
3. The Mac Accepts No New Inbound Connection
The Mac initiates the SSH session. Model requests return as channels inside that established session, so the Mac’s router needs no port-forwarding rule and the Mac firewall needs no Ollama allowance.
4. The Application Remains the Policy Enforcement Point
Users reach only the HTTPS application. It performs login, tenant checks, document authorization, rate limiting, validation, and audit policy before creating a model prompt. The tunnel does not bypass application authorization.
5. SSH Can Be Constrained to One Purpose
A dedicated VPS account and key reduce the impact of a leaked credential. A Match User block can allow only remote forwarding, disable TTY, X11, and agent forwarding, and restrict the listener with PermitListen.
Be Precise About the Remaining Risks
| Risk | What this design does | What you still need to do |
|---|---|---|
| Internet scan of 11434/11435 | No routable listener exists | Continuously verify loopback binds |
| Network eavesdropping | HTTPS and SSH encrypt traffic | Protect certificates, keys, and host-key verification |
| Compromised VPS | Attacker may access VPS-local 11435 | Patch the VPS, isolate tenants, and minimize privileges |
| Stolen SSH private key | A dedicated key limits blast radius | Use restrictions, passphrase or Keychain, rotation, and revocation |
| Malicious or oversized prompts | The tunnel transports them faithfully | Authenticate, authorize, validate, rate-limit, and cap size |
| Sensitive prompt logging | Encryption protects transit, not stored logs | Redact content and define retention rules |
| Mac sleep or network loss | The tunnel becomes unavailable | Monitor health and provide a safe failure or fallback path |
Preflight Security Checklist
- Ollama is bound only to
127.0.0.1on the Mac. - The reverse tunnel binds only to
127.0.0.1on the VPS. GatewayPortsisno.- Port 11435 is absent from public firewall and cloud security-group rules.
- The Mac router has no Ollama port-forwarding rule.
- The application allow-lists its localhost Ollama base URL.
- The tunnel uses a dedicated, non-root SSH user and key.
- SSH host-key checking remains enabled.
- Browser users are authenticated, authorized, and rate-limited.
- Prompt and response logs follow a defined retention policy.
- The application handles tunnel failure without exposing diagnostics.
- Any cloud fallback is allowed by data policy for that request.
Operations and Troubleshooting
The Mac is part of the serving path, so monitor the chain: local Ollama health, tunnel health from the VPS, a small generation request, and a complete browser-to-answer transaction. Record RAG time, model time, and total time separately.
Use bounded retries with jitter instead of allowing failed browser requests to create a retry storm. If a cloud fallback is available, make it an explicit policy choice: permission to use a private Mac model does not automatically authorize sending the same context to a third party.
| Symptom | Most likely cause | Check or fix |
|---|---|---|
Mac local /api/tags fails | Ollama is not running | Start Ollama; verify ollama list and port 11434 |
remote port forwarding failed | Port occupied or forwarding denied | Check ss -lnt, AllowTcpForwarding, and PermitListen |
| VPS gets connection refused on 11435 | Tunnel is down | Inspect the Mac SSH process and LaunchAgent error log |
| Tags work but generation fails | Model missing, too large, or malformed request | Check ollama list, memory pressure, and Ollama logs |
| Tunnel dies after idle time | NAT timeout, sleep, or network change | Use SSH keepalives, KeepAlive, and suitable Mac power settings |
| App fails while VPS curl works | App URL, timeout, or permissions | Confirm 127.0.0.1:11435 and inspect application logs |
| Public port 11435 answers | Unsafe bind or firewall exposure | Stop the tunnel; restore loopback and GatewayPorts no |
| First answer is much slower | Model cold start | Warm the model or account for load time in the user experience |
# On the Mac
curl -fsS http://127.0.0.1:11434/api/tags
launchctl print gui/$(id -u)/com.jamd.ollama-tunnel
tail -f ~/Library/Logs/JAMDOllamaTunnel.err.log
# From the Mac, testing the VPS side
ssh helpnow-ollama-vps "ss -lnt | grep ':11435'"
ssh helpnow-ollama-vps "curl -fsS http://127.0.0.1:11435/api/tags"
When Not to Use This
This pattern is not ideal when guaranteed 24/7 availability is required and the Mac may sleep or move, when many simultaneous users need fast responses, or when compliance rules prohibit prompts from being sent to a workstation.
Where This Fits Best
- Internal RAG assistants
- Customer support prototypes
- Private company knowledge-base search
- Model comparison labs
- Low-volume browser AI tools
- Local-first AI experiments
- Cost-controlled demos
Final Thoughts
The best infrastructure ideas often come from separating responsibilities cleanly. A VPS is excellent at being a public server. A modern Mac is excellent at local inference. An SSH reverse tunnel lets each machine do what it is good at.
For JAMD Technologies, this creates a practical path forward: public, browser-based AI apps in the cloud, private local model capacity on Apple Silicon, and frontier cloud models available when we need maximum reliability or quality.
That is the kind of hybrid architecture we like: simple, secure, cost-aware, and ready to improve as the hardware gets faster.