Securely Expose Your Home Lab with Cloudflare Tunnel (Zero Trust) on Ubuntu and Docker

Overview

This step-by-step guide shows you how to publish a private web service to the internet securely using Cloudflare Tunnel (cloudflared) and Cloudflare Zero Trust—without opening inbound ports on your router. You will run the tunnel in Docker on Ubuntu, route your domain through Cloudflare, protect the app with Single Sign-On (SSO), and optionally verify JSON Web Tokens (JWT) at the application layer. This approach adds strong security, free TLS, DDoS protection, and granular access control to your self-hosted apps.

Prerequisites

You need an Ubuntu 22.04 or 24.04 server (bare metal or VM), Docker and Docker Compose installed, a domain you control, and a Cloudflare account. If your domain is not already on Cloudflare, change your registrar’s nameservers to Cloudflare’s to manage DNS and Zero Trust features.

Step 1 — Prepare your domain on Cloudflare

1. Log in to Cloudflare and add your domain if you have not already. Follow the prompts to update nameservers at your registrar. Propagation may take a few minutes.

2. In the Cloudflare dashboard, verify that DNS is active and the orange cloud is enabled for your root or subdomains.

Step 2 — Enable Zero Trust

1. Open the Zero Trust dashboard (also labeled “Zero Trust” or “Access” in Cloudflare). Create your Zero Trust account if prompted.

2. Under Settings > Authentication, connect an identity provider (e.g., Google, Microsoft Entra ID, GitHub) or enable the One-Time Pin option for quick protection.

Step 3 — Install Docker and Compose (if needed)

If Docker is not installed, use the official convenience script. Then enable and test it. Example:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
docker --version

Install Docker Compose v2 if it is not present (many modern Docker packages include it as docker compose):

docker compose version

Step 4 — Create a Tunnel in the Cloudflare Dashboard

1. In Zero Trust > Networks > Tunnels, click “Create a tunnel,” choose “Cloudflared,” name it (e.g., homelab-tunnel), and create it.

2. Copy the provided token. You will use this token in Docker to authenticate the tunnel without interactive login.

Step 5 — Run cloudflared in Docker

Create a working directory, then a docker-compose.yml file. Replace the placeholder token with yours.

mkdir -p ~/cloudflared && cd ~/cloudflared
nano docker-compose.yml

version: "3.8"
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel run
    restart: unless-stopped
    environment:
      - TUNNEL_TOKEN=<PASTE_YOUR_TUNNEL_TOKEN>
    volumes:
      - ./config:/etc/cloudflared

Bring it up:

docker compose up -d

The container will authenticate with Cloudflare automatically using the token. You should see the tunnel listed as “Healthy” in the Zero Trust dashboard within a minute.

Step 6 — Configure ingress rules to publish your service

Create a config file to route a public hostname to your internal service. For example, map app.yourdomain.com to a local web app on port 8080 and ha.yourdomain.com to Home Assistant on port 8123.

mkdir -p config
nano config/config.yml

tunnel: <YOUR_TUNNEL_ID>
credentials-file: /etc/cloudflared/<YOUR_TUNNEL_ID>.json
ingress:
  - hostname: app.yourdomain.com
     service: http://host.docker.internal:8080
  - hostname: ha.yourdomain.com
     service: http://host.docker.internal:8123
  - service: http_status:404

On Linux, host.docker.internal may not resolve by default. Use the host IP (e.g., http://192.168.1.50:8080) or attach the tunnel to the same Docker network as your app containers and reference them by service name (e.g., http://web:8080).

Restart the tunnel to apply changes:

docker compose restart

Step 7 — Route DNS to the tunnel

Back in Zero Trust > Networks > Tunnels, open your tunnel and add Public Hostnames. Enter app.yourdomain.com and select the corresponding service. Cloudflare will automatically create proxied DNS records and issue valid TLS certificates.

Step 8 — Protect the app with Cloudflare Access (SSO/MFA)

1. Go to Zero Trust > Access > Applications > Add an application > Self‑hosted.

2. Set the application domain to app.yourdomain.com, pick a name, and click Next.

3. Create a policy: allow emails from your domain (e.g., [email protected]) or a group from your IdP. Optionally require MFA with Cloudflare’s TOTP or your IdP’s enforced MFA.

4. Save. Now your app is behind an identity-aware proxy. Only authenticated users you choose can reach it.

Optional — Verify JWT in your application or reverse proxy

Cloudflare Access sends a signed JWT in the CF-Access-Jwt-Assertion header. Your app or Nginx can verify it for an extra layer of defense-in-depth. In Nginx, pass the header to upstream or validate with an auth_request service. If you use languages like Node.js, Python, or Go, verify using the public JWKs at https://<your-team>.cloudflareaccess.com/cdn-cgi/access/certs and check audience and issuer claims.

Monitoring and troubleshooting

Check health: In the Cloudflare dashboard, your tunnel should be Healthy. On the host, view logs with docker logs -f <container_name>.

403 after login: Ensure your Access policy allows your email or group, and that the application domain matches the hostname you visit.

Bad gateway: Verify the internal service address and port in config.yml. Confirm the service is reachable from the tunnel container’s network.

DNS mismatch: Confirm the Public Hostname in the tunnel matches the DNS entry and the Access application domain.

IPv6 or CGNAT: Cloudflare Tunnel works without inbound ports, even behind CGNAT. No router changes are needed.

Security best practices

Use strong identity controls with MFA and short Access session durations. Limit policies to specific users or groups, not anyone with the link. Avoid exposing administrative apps publicly unless protected by Access. Keep cloudflared updated by periodically pulling the latest Docker image, and restrict who can manage your Cloudflare account with role-based access.

Conclusion

With Cloudflare Tunnel and Zero Trust, you can publish internal apps safely without port forwarding or complicated firewall rules. You get automatic TLS, DDoS protection, identity-based access, and optional JWT validation. This modern pattern is ideal for homelabs, small businesses, and remote teams that need secure, simple access to self‑hosted services.

Deploy Ollama + Open WebUI with NVIDIA GPU on Ubuntu using Docker Compose and Nginx (HTTPS-ready)

Overview

This step-by-step guide shows you how to deploy an AI chatbot stack with Ollama (for running local LLMs) and Open WebUI (a clean, browser-based interface) on Ubuntu 22.04 or 24.04. We will run both apps in Docker, enable NVIDIA GPU acceleration, and put Nginx in front with a free TLS certificate from Let's Encrypt. You will get a production-friendly setup with persistent storage, HTTPS, and simple maintenance commands.

What you'll build

You will end up with two containers on a private Docker network: ollama (listening on 11434) and openwebui (listening on 8080, mapped to localhost:3000). Nginx will reverse proxy a public domain (for example, ai.example.com) to Open WebUI and handle SSL. Models and chat data will be stored on the host so updates don't wipe them.

Prerequisites

- Ubuntu 22.04/24.04 with sudo access
- An NVIDIA GPU (Turing or newer recommended) and a supported driver
- A DNS A record pointing your domain (e.g., ai.example.com) to your server's public IP
- Outbound internet access to pull images and models

1) Install NVIDIA driver and container toolkit

Update the system and install the proprietary driver. If you don't already have the correct driver, Ubuntu can choose one for you:
sudo apt update && sudo apt -y upgrade
sudo ubuntu-drivers autoinstall
Reboot:
sudo reboot

After reboot, confirm the GPU is visible:
nvidia-smi
Install the NVIDIA Container Toolkit so Docker can use the GPU:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update && sudo apt -y install nvidia-container-toolkit
Configure Docker to use it and restart Docker:
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

2) Install Docker Engine and Docker Compose plugin

Install the official Docker packages:
sudo apt -y install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify:
docker --version
docker compose version

3) Create persistent folders and a Docker Compose file

Create directories for persistent data:
sudo mkdir -p /opt/ollama /opt/openwebui
sudo chown -R $USER:$USER /opt/ollama /opt/openwebui
Now create a compose.yml in a new project folder (for example, /opt/ai-stack/compose.yml) with the following content:

version: "3.8"
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
volumes:
- /opt/ollama:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
networks:
- ai
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

openwebui:
image: ghcr.io/open-webui/open-webui:latest
container_name: openwebui
restart: unless-stopped
depends_on:
- ollama
environment:
- OLLAMA_BASE_URL=http://ollama:11434
ports:
- "127.0.0.1:3000:8080"
volumes:
- /opt/openwebui:/app/backend/data
networks:
- ai

networks:
ai:

Notes: We bind Open WebUI to localhost:3000 so it is not exposed directly. Nginx will handle public traffic. The NVIDIA device reservation passes the GPU into the Ollama container. If your Docker Compose version supports it, you may also use gpus: all under the ollama service instead of the deploy block.

4) Start the stack and test

From the folder with compose.yml, bring the stack up:
docker compose up -d
Watch logs until both services are healthy:
docker compose logs -f
Pull a model and perform a quick GPU test (you should see GPU usage spike in nvidia-smi):
docker exec -it ollama ollama pull llama3:8b
docker exec -it ollama ollama run llama3:8b
Locally, you can visit Open WebUI at http://127.0.0.1:3000. Next, we'll put it behind HTTPS.

5) Install Nginx and obtain a Let's Encrypt certificate

Install Nginx and Certbot:
sudo apt -y install nginx certbot python3-certbot-nginx
Create an Nginx server block (replace ai.example.com with your domain):
sudo nano /etc/nginx/sites-available/ai.conf
Paste:

server {
listen 80;
server_name ai.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
client_max_body_size 20m;
}

Enable and test:
sudo ln -s /etc/nginx/sites-available/ai.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Issue a certificate and force HTTPS:
sudo certbot --nginx -d ai.example.com --redirect --agree-tos -m [email protected]
Now browse to https://ai.example.com and you should see Open WebUI served over TLS.

6) Backups, updates, and security tips

Back up data by archiving the two directories we created:
sudo tar -czf /root/ollama-backup.tgz /opt/ollama
sudo tar -czf /root/openwebui-backup.tgz /opt/openwebui
Open WebUI stores conversations and settings in /opt/openwebui; Ollama stores models and blobs in /opt/ollama.

To update images with minimal downtime:
docker compose pull
docker compose up -d
Old images can be cleaned with docker image prune when you're done testing. For security, keep ports private (we only published 3000 to localhost) and use your firewall to allow 80/443 only. If you need extra protection, add HTTP Basic Auth to Nginx and restrict by IP when possible.

Troubleshooting

- GPU not used: watch nvidia-smi while running a model. If it stays idle, recheck the NVIDIA driver, container toolkit, and the GPU reservation in compose.yml.
- Models fail due to VRAM limits: try smaller variants (e.g., llama3:8b instead of 70B) or quantized builds (like q4_K_M).
- Port conflicts: change the host port mapping in compose.yml if 3000 is taken (e.g., use 127.0.0.1:3100:8080 and update the Nginx proxy_pass accordingly).
- Certbot issues: make sure your domain points to the server’s public IP and TCP/80 is reachable from the internet during certificate issuance.

What's next

From here, you can connect more tools to the Ollama API, add multiple models, fine-tune your Nginx headers, or place the stack behind Cloudflare. The setup is simple to maintain: pull updates, restart the stack, and your data persists. With GPU acceleration and HTTPS in place, you have a fast, private AI assistant ready for everyday use.

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...