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., you@yourcompany.com) 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.

Comments