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

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

- URL: https://codethepixel.com/zero-trust-remote-access-exposing-self-hosted-apps-safely-with-cloudflare-tunnel
- Author: Asyraf Wahi Anuar (https://codethepixel.com/author/asyraf-wahi-anuar)
- Published: 2026-11-14
- Category: Home Server
- Level: Intermediate
- Tested with: Ubuntu Server 26.04.1 LTS, Docker, Cloudflare
- Tags: Docker, Home Server, Ubuntu, Cloudflare, Tunnel
- Reading time: 5 min

## 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.

In the previous post, "[Installing Ubuntu Server 26.04.1 LTS & Essential Docker Stack](https://codethepixel.com/installing-ubuntu-server-26041-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:

```
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:

```
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:

```
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:

```
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.
