Skip to content
CODE THE PIXEL
Home Server

Zero-Trust Remote Access: Exposing Self-Hosted Apps Safely with Cloudflare Tunnel

Asyraf Wahi Anuar 5 min read Intermediate

Learn to expose self-hosted Docker apps without opening router ports using Cloudflare Tunnel, then protect admin dashboards with Cloudflare Access policies.

Key takeaways

  • Cloudflare Tunnel uses the cloudflared daemon to create an outbound-only encrypted connection from your server to Cloudflare's edge.
  • Create a shared Docker network with docker network create tunnel-net so cloudflared can route to containers by internal name.
  • Deploy cloudflared with Docker Compose, storing the tunnel token in a .env file with chmod 600 permissions.
  • Add public hostnames in the tunnel's Public Hostname tab, pointing HTTP services to internal container names and ports.
  • Protect admin dashboards with Cloudflare Access self-hosted applications and an Allow policy restricted to your email.

Tech stack

  • Ubuntu Server 26.04.1 LTS
  • Docker
  • Cloudflare

In the previous post, "Installing Ubuntu Server 26.04.1 LTS & Essential Docker Stack", I locked down the operating system with UFW and stood up my core Docker management and monitoring tools. Up to this point, everything has lived strictly within the local home network.

To access these services from outside the house or to host public-facing web applications, the traditional approach involves opening ports 80 and 443 on your home router, configuring Dynamic DNS (DDNS) to track ISP IP address changes, and exposing your residential IP address to the public internet.

Instead, I went with Cloudflare Tunnel.

1. How Cloudflare Tunnel Works (And Why It Beats Port Forwarding)

A Cloudflare Tunnel creates a secure, encrypted, outbound-only connection from your home server to Cloudflare's global edge network using a lightweight daemon called cloudflared.

  • Zero Open Ports: Your router's firewall stays completely closed to incoming traffic. Because cloudflared initiates the connection from inside your network outward, attackers cannot scan or target open ports on your home router.

  • Hidden Residential IP: Public DNS records point to Cloudflare's edge servers rather than your home IP address, masking your physical location and shielding your connection from DDoS attacks.

  • Automatic SSL/TLS: Cloudflare automatically handles HTTPS certificates at the edge, eliminating manual certificate renewal headaches.

  • Dynamic IP Resilience: Even when your ISP rotates your home IP address, the outbound tunnel automatically reconnects without breaking your subdomains.

2. Prerequisites

Before deploying the connector container, make sure you have:

  1. A Domain on Cloudflare: Your domain's nameservers must be active on a free Cloudflare account.

  2. A Shared Docker Network: Running cloudflared on the same custom Docker bridge network as your applications allows it to route traffic using internal container names (e.g., http://uptime-kuma:3001) without binding ports to the host machine.

Create a dedicated Docker network on your server if you haven't already:

Plain text
docker network create tunnel-net

3. Creating the Tunnel in Cloudflare Zero Trust

  1. Log in to the Cloudflare Zero Trust Dashboard.

  2. Navigate to Networks → Tunnels and click Create a tunnel.

  3. Select Cloudflared as the connector type and click Next.

  4. Give your tunnel a recognizable name (e.g., homeserver-tunnel) and click Save tunnel.

  5. On the Install and run a connector screen, select Docker. Copy only the token string after --token (it begins with eyJ...). Treat this token like a password.

4. Deploying cloudflared via Docker Compose

Rather than running a standalone docker run command, deploying cloudflared with Docker Compose keeps your configuration declarative, restarts cleanly on reboot, and keeps your token out of shell history.

Create a dedicated directory and .env file on your server:

Plain text
mkdir -p ~/docker/cloudflared
cd ~/docker/cloudflared
echo "TUNNEL_TOKEN=your_copied_token_here" > .env
chmod 600 .env

Next, create your docker-compose.yml file:

Plain text
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    networks:
      - tunnel-net
networks:
  tunnel-net:
    external: true

Start the container and verify the connection:

Plain text
docker compose up -d
docker compose logs -f cloudflared

Once you see Registered tunnel connection in the logs, head back to the Cloudflare Zero Trust dashboard; your tunnel status should now show Healthy.

5. Routing Subdomains to Internal Containers

With the tunnel active, exposing a self-hosted service takes less than a minute:

  1. In your tunnel configuration page on Cloudflare, open the Public Hostname tab and click Add a public hostname.

  2. Fill in your desired subdomain and domain (e.g., status.yourdomain.com).

  3. Under Service, select HTTP as the type and enter the internal Docker container name and port (for example, uptime-kuma:3001 or beszel:8090).

  4. Click Save hostname. Cloudflare automatically creates the proxied CNAME DNS record and provisions an SSL certificate.

Ensure any container you want to route through the tunnel is also attached to the tunnel-net Docker network in its own docker-compose.yml file.

6. Locking Down Admin Dashboards with Cloudflare Access

Exposing a public web app is one thing; exposing infrastructure tools like Portainer, Dockhand, or Beszel directly to the internet is risky, even with strong local passwords.

To add a second perimeter layer, I placed Cloudflare Access (Zero Trust) in front of sensitive admin subdomains:

  1. In Cloudflare Zero Trust, go to Access → Applications and click Add an application → Self-hosted.

  2. Enter the admin subdomain (e.g., portainer.yourdomain.com).

  3. Create an Allow policy using One-time PIN (or Google/GitHub OAuth) restricted strictly to your personal email address.

Now, anyone visiting your admin subdomain hits Cloudflare's authentication gate first unauthorized traffic never even reaches your home server's tunnel.

What's Next?

With Cloudflare Tunnel and Zero Trust Access in place, the home server now has enterprise-grade external routing with no ports exposed on the router. In the next post, I'll look at deploying actual production web applications and automating backups so the server runs reliably without manual intervention.

Did this tutorial help?

0 readers found this helpful

Comments

No comments yet. Questions about this tutorial are welcome here: the answer helps the next reader too.

Leave a comment

Markdown works: `code`, ``` blocks, **bold**, links.

First comments are reviewed before they appear.

Docker Intermediate

Docker Cheat Sheet: Standard & Docker Compose Commands

A concise Docker cheat sheet of essential CLI commands for containers, images, logs, and Docker Compose, plus system cleanup commands like docker system prune.

2 min read

With your permission, this site uses Google Analytics cookies to count visits and see which tutorials are read. Nothing is set until you accept. Privacy Policy