
You can run a full chat UI on your VPS without exposing inbound ports to the public internet. This guide walks through an Open WebUI Cloudflare Tunnel setup. Open WebUI runs locally in Docker, Cloudflare Tunnel publishes it, and your origin stays closed.
This setup works well for a private, ChatGPT-style interface for a small team. You can place Cloudflare Access/SSO in front and keep networking simple. You also get an easy rollback path and a short hardening checklist.
What you’ll build (and what you won’t)
- Build: Open WebUI on a VPS, reachable at
https://chat.yourdomain.comthrough Cloudflare Tunnel. - No inbound ports: Your VPS firewall can block 80/443 entirely. The tunnel makes an outbound connection to Cloudflare.
- Optional protection: Cloudflare Access (SSO, one-time PIN, email allowlist) in front of the app.
- Honest limitation: Cloudflare Tunnel is a Cloudflare dependency. If Cloudflare is down for your zone, access is impacted.
Prerequisites (VPS, domain, and a realistic sizing note)
Open WebUI itself is light. The resource cost usually comes from your model backend (Ollama/vLLM/llama.cpp).
This tutorial focuses on the UI and the secure access pattern.
- VPS: Ubuntu 22.04/24.04 LTS or Debian 12 with root or sudo access.
- Minimum for UI-only: 2 vCPU, 2–4 GB RAM, 20 GB disk.
- For local inference (optional): Plan separately for RAM/VRAM. For a 7B–8B class model on CPU, 16 GB RAM is a comfortable starting point. GPU requirements depend on VRAM and quantization.
- Domain in Cloudflare: Your domain’s DNS is managed by Cloudflare.
- Docker: Installed on the VPS.
If you want a clean VPS to start from, a HostMyCode VPS is a straightforward fit for this setup.
If you’d rather not handle OS updates and baseline hardening yourself, consider managed VPS hosting.
Architecture choices: local model vs remote model
Open WebUI is the front end. It still needs a model endpoint behind it.
- Option A (simple): Open WebUI connects to a local Ollama on the same VPS (best for a single box).
- Option B (safer separation): Open WebUI runs on a small VPS and connects to a separate inference server (GPU/dedicated).
This tutorial assumes Option A is feasible. The tunnel approach is the same for Option B.
If you’re planning local inference with Ollama, see our existing guide on deploying it with TLS on Debian: Deploy Ollama on Debian with Docker and TLS.
Step 1: Prepare the VPS (users, updates, firewall stance)
Update packages and install a few baseline tools:
sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw
If you haven’t enabled automatic security updates, do that next. It’s one of the highest-value changes for an internet-connected server.
Follow: Set Up Automated Security Updates on Ubuntu VPS.
Firewall note: Cloudflare Tunnel uses outbound connections. You can keep inbound locked down.
At minimum, allow SSH from your admin IP:
# Replace 203.0.113.10 with your admin IP
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp
sudo ufw enable
If you can’t pin an admin IP, use SSH keys and tighten this later.
Don’t leave password SSH open to the world.
Step 2: Install Docker (if you don’t already have it)
If Docker is already installed, skip this step. Just confirm it works with docker ps.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
newgrp docker
Verify:
docker version
docker compose version
Step 3: Run Open WebUI with Docker Compose
Create a working directory:
sudo mkdir -p /opt/openwebui
sudo chown -R $USER:$USER /opt/openwebui
cd /opt/openwebui
Create docker-compose.yml:
cat > docker-compose.yml <<'YAML'
services:
openwebui:
image: ghcr.io/open-webui/open-webui:main
container_name: openwebui
restart: unless-stopped
environment:
# If you're using local Ollama, keep this. Otherwise point to your remote LLM endpoint.
- OLLAMA_BASE_URL=http://ollama:11434
# Open WebUI stores data here
- WEBUI_SECRET_KEY=change-me-to-a-long-random-string
volumes:
- openwebui-data:/app/backend/data
ports:
# Bind only to localhost so it's not reachable from the public network
- "127.0.0.1:3000:8080"
# Optional: local Ollama for model inference
ollama:
image: ollama/ollama
container_name: ollama
restart: unless-stopped
volumes:
- ollama:/root/.ollama
ports:
# Keep Ollama local-only as well
- "127.0.0.1:11434:11434"
volumes:
openwebui-data:
ollama:
YAML
Start it:
docker compose up -d
docker compose ps
Quick local test from the VPS:
curl -I http://127.0.0.1:3000
You should see an HTTP response (often 200 or a redirect). If you don’t, check logs:
docker logs --tail=100 openwebui
Step 4: Create a Cloudflare Tunnel (cloudflared) on the VPS
Cloudflare Tunnel runs through cloudflared. It maintains an outbound connection to Cloudflare.
You’ll create a named tunnel. Then you’ll route a hostname to it.
Install cloudflared on Ubuntu/Debian using Cloudflare’s repository. Commands can vary by distro, but this pattern is usually reliable:
# 1) Add Cloudflare package signing key
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
# 2) Add repository (Debian/Ubuntu compatible)
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/ $(. /etc/os-release && echo $VERSION_CODENAME) main" | sudo tee /etc/apt/sources.list.d/cloudflare-main.list
sudo apt update
sudo apt -y install cloudflared
If your distro codename isn’t available in Cloudflare’s repo, use Cloudflare’s official install method for your OS.
The rest of the steps don’t change.
Step 5: Authenticate cloudflared to your Cloudflare account
Run:
cloudflared tunnel login
This prints a URL. Open it in your browser, log into Cloudflare, and authorize the domain/zone.
Cloudflared downloads credentials to the server (typically under ~/.cloudflared/).
Tip: If you prefer strict separation, do this as a dedicated system user. On a single-purpose VPS, using your admin account is usually fine.
Step 6: Create the tunnel and map a hostname
Create a named tunnel:
cloudflared tunnel create openwebui
List tunnels to confirm:
cloudflared tunnel list
Pick a hostname you want to use, for example chat.example.com. Then route DNS to the tunnel:
# Replace chat.example.com with your hostname
cloudflared tunnel route dns openwebui chat.example.com
This creates or updates a DNS record in Cloudflare (usually a CNAME to the tunnel). You do not need to open ports 80/443 on the VPS.
If you’re still moving DNS to Cloudflare, do that first. Our tutorial covers the careful steps to avoid downtime: Migrate a Domain to Cloudflare DNS Without Downtime.
Step 7: Configure cloudflared to forward to Open WebUI
Create a config directory and file. Cloudflared reads this file for ingress rules.
sudo mkdir -p /etc/cloudflared
# Find your tunnel UUID in ~/.cloudflared/*.json
ls -la ~/.cloudflared
Create /etc/cloudflared/config.yml (adjust hostname):
sudo tee /etc/cloudflared/config.yml > /dev/null <<YAML
tunnel: openwebui
credentials-file: /root/.cloudflared/REPLACE_WITH_YOUR_TUNNEL_UUID.json
ingress:
- hostname: chat.example.com
service: http://127.0.0.1:3000
- service: http_status:404
YAML
Important: The credentials-file path must match where cloudflared stored your tunnel JSON.
If you authenticated as a non-root user, the file will be under that user’s home directory. Do one of the following:
- Move the credentials to
/root/.cloudflared/, or - Run the systemd service as that user and point to the correct path.
Make sure the credentials are readable only by root:
sudo chmod 600 /etc/cloudflared/config.yml
sudo chmod 600 /root/.cloudflared/*.json
Step 8: Run cloudflared as a systemd service
Install the service:
sudo cloudflared service install
Then start and enable it:
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager
Tail logs if needed:
sudo journalctl -u cloudflared -f
Now browse to https://chat.example.com.
Cloudflare terminates TLS at the edge. It then forwards traffic through the tunnel to your localhost service.
Step 9: Add Cloudflare Access (SSO / OTP) in front of Open WebUI
Open WebUI includes its own auth features. Cloudflare Access gives you a cleaner perimeter.
It also keeps the hostname from becoming a public login page.
In the Cloudflare dashboard:
- Go to Zero Trust (Cloudflare One) → Access → Applications.
- Add an application → Self-hosted.
- Set the domain to
chat.example.com. - Create an Access Policy:
- Allow emails in your company domain, or specific addresses.
- Optionally require a device posture check if you use it.
- Enable one-time PIN if you want the lightest setup.
Once enabled, users must pass Access checks before Open WebUI loads. This reduces brute-force traffic and credential stuffing attempts that would otherwise hit the app.
Step 10: Pull a small model (optional) and verify Open WebUI can chat
If you enabled the Ollama container, pull a model. Start small if the VPS has limited RAM.
# Example: pull a model inside the ollama container
docker exec -it ollama ollama pull llama3
Then open Open WebUI and select the model.
If the UI can’t see Ollama, check the basics:
- The Open WebUI container has
OLLAMA_BASE_URL=http://ollama:11434. - Both services are on the same Compose network (default behavior).
- Ollama is healthy:
docker logs --tail=100 ollama.
If you’re choosing quantization for CPU or limited VRAM, this guide walks through the tradeoffs: llama.cpp Quantization: Pick Q4 vs Q5 vs Q8.
Hardening checklist for a private AI UI
Most self-hosting security issues come from basic web hygiene. Knock these out early.
- Keep origin services on localhost: as shown, Open WebUI and Ollama bind to
127.0.0.1. - Block inbound ports: UFW default deny incoming; only SSH from a trusted IP.
- Disable password SSH: use keys, and consider moving SSH to a non-default port if you’re a common target.
- Rotate secrets: set a long
WEBUI_SECRET_KEYand store it in a root-only file or a secrets manager. - Turn on Cloudflare Access: even a simple email allowlist reduces exposure sharply.
- Watch logs: Cloudflare Access logs +
journalctl -u cloudflared+ container logs.
If you later publish an inference API (vLLM/Ollama) publicly, treat it like any production API. Use auth keys, rate limits, and strict allowlists.
For those patterns, see: Secure vLLM API With Nginx, Keys, and Rate Limits.
Common problems and quick diagnostics
Cloudflare Access page loops or never reaches Open WebUI
- Check Cloudflare Zero Trust policy: make sure your user matches an Allow rule.
- Confirm the Access app domain exactly matches
chat.example.com. - Clear cookies for the domain and try again.
502/504 errors from Cloudflare
This usually means the tunnel can’t reach your local service.
# On the VPS
curl -I http://127.0.0.1:3000
sudo systemctl status cloudflared --no-pager
sudo journalctl -u cloudflared --since "10 minutes ago" --no-pager
If you get a local response but Cloudflare still errors, re-check /etc/cloudflared/config.yml hostname and service URL.
Also confirm DNS is routed to the correct tunnel.
For Nginx-related 502 debugging patterns (useful if you add Nginx later), this guide is a solid reference: Nginx 502 Bad Gateway Fix on Ubuntu VPS.
Open WebUI loads, but models don’t appear
- Verify Ollama is reachable from inside the Open WebUI container:
docker exec -it openwebui sh -lc "apk add --no-cache curl || true; curl -s http://ollama:11434/api/tags | head"
- If you use a remote model server, ensure routing and auth are correct, and that your endpoint is reachable from the VPS.
Operational notes: backups, updates, and upgrades
Open WebUI stores state in a Docker volume (openwebui-data).
Back it up before major upgrades or config changes.
A simple approach is to snapshot the volume to a tarball:
mkdir -p /opt/backups/openwebui
docker run --rm \
-v openwebui-openwebui-data:/data:ro \
-v /opt/backups/openwebui:/backup \
alpine sh -lc "cd /data && tar -czf /backup/openwebui-data-$(date +%F).tar.gz ."
If you extend this stack with PostgreSQL (for RAG or other app data), back up the database and test restores regularly.
This tutorial shows a good restore-test workflow: Set Up PostgreSQL Backups With pg_dump + Restore Test.
To update containers:
cd /opt/openwebui
docker compose pull
docker compose up -d
docker image prune -f
Summary: a private AI chat UI without opening ports
You now have Open WebUI on your VPS, published through Cloudflare Tunnel, and optionally gated by Cloudflare Access.
The core idea is simple. Bind services to localhost and let the tunnel handle ingress.
If you want this running on stable infrastructure with enough headroom for model testing, start with a HostMyCode VPS.
If you’d rather hand off patching, monitoring, and baseline security, managed VPS hosting is often the easier route.
If you’re building a private AI chat stack for your team, keep it on infrastructure you control and can scale. A HostMyCode VPS is a solid starting point for Open WebUI + Cloudflare Tunnel, and you can move inference to a larger node as usage grows.
Prefer fewer moving parts to own day-to-day? Use managed VPS hosting and spend your time on the application layer instead of the base server.
FAQ
Do I need to open ports 80 or 443 on my VPS for Cloudflare Tunnel?
No. Cloudflare Tunnel works over outbound connections from the VPS to Cloudflare. You can keep inbound 80/443 closed. You still need SSH access for administration.
Is Cloudflare Access required, or is Open WebUI login enough?
Open WebUI login helps, but Cloudflare Access blocks the app before it’s reachable. For private team tools, Access is usually worth enabling to reduce exposure and logins hitting your origin.
Can I run Open WebUI on a small VPS and keep the model on a GPU box?
Yes. Point Open WebUI at your remote inference endpoint instead of http://ollama:11434. This keeps the UI cheap and lets you scale model hardware independently.
What’s the safest way to restrict who can reach the UI?
Use Cloudflare Access with an email allowlist or SSO, keep Open WebUI bound to localhost, and allow SSH only from known IPs. That combination blocks most unwanted traffic before it reaches your server.
What data should I back up?
At minimum, back up the Open WebUI data volume and any model/config state you care about. If you add a database for RAG, back up that database with restore tests, not just dumps.