Publishing self-hosted apps on the internet without opening ports is now simple and safe with Cloudflare Tunnel and Nginx Proxy Manager (NPM). This guide shows you how to route traffic from your domain through Cloudflare’s global network to your internal services, using Docker on Linux. You will get HTTPS by default, Zero Trust access, and simple management for multiple apps.
Why use Cloudflare Tunnel + Nginx Proxy Manager?
Cloudflare Tunnel (cloudflared) creates an outbound-only connection from your server to Cloudflare, so you do not need port forwarding or a public IP. Nginx Proxy Manager provides a friendly UI to reverse-proxy multiple services, manage SSL, and handle redirects and headers. Together, they offer a secure and flexible edge-to-origin pipeline for home labs and small businesses.
Prerequisites
- A domain managed by Cloudflare (nameservers must point to Cloudflare)
- A Linux server (Ubuntu 22.04+ recommended) with Docker and Docker Compose installed
- Basic familiarity with terminal and Docker
Architecture overview
Cloudflare edge terminates TLS and forwards requests over an encrypted tunnel to the cloudflared container on your server. The cloudflared service forwards hostnames to Nginx Proxy Manager, which then routes requests to internal applications (e.g., Home Assistant, Portainer, Jellyfin) based on hostnames. This design centralizes access, certificates, logging, and security rules.
Step 1 — Install Docker and Compose (Ubuntu)
Run the following commands to set up Docker:
sudo apt update
sudo apt install -y 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 $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
Step 2 — Create a Cloudflare Tunnel
1) In the Cloudflare dashboard, open Zero Trust (or Tunnels) and create a new Tunnel named “homelab”.
2) Choose “Docker” as the environment and copy the token-based command or credentials JSON for cloudflared. We will use the token method for simplicity.
3) Do not add routes yet—we will define them in the docker-compose file.
Step 3 — Create docker-compose.yml
Create a working directory (e.g., /opt/homelab) and add this compose file. Replace example.com with your domain and paste your Cloudflare tunnel token.
version: "3.8"
services:
cloudflared:
image: cloudflare/cloudflared:2024.8.3
command: tunnel run
environment:
- TUNNEL_TOKEN=<PASTE_YOUR_TUNNEL_TOKEN>
restart: unless-stopped
healthcheck:
test: ["CMD", "cloudflared", "version"]
interval: 30s
timeout: 10s
retries: 3
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "81:81" # NPM admin UI
- "80:80" # HTTP
- "443:443" # HTTPS
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- cloudflared
networks:
default:
name: homelab
Start the stack:
docker compose up -d
docker compose logs -f cloudflared
Step 4 — Map hostnames to Nginx Proxy Manager
Back in the Cloudflare dashboard, open your Tunnel and add public hostnames to route traffic to NPM. For each app, create a route like this:
Example routes:
- app1.example.com → http://npm:80
- media.example.com → http://npm:80
- portainer.example.com → http://npm:80
This means Cloudflare forwards incoming requests for those hostnames down the tunnel to the NPM container.
Step 5 — Configure Nginx Proxy Manager
1) Open http://YOUR_SERVER_IP:81 and log in (default admin credentials are shown on first boot; change them immediately).
2) For each internal service, create a “Proxy Host”:
- Domain Names: app1.example.com
- Scheme: http
- Forward Hostname/IP: the internal container name or IP (e.g., homeassistant or 127.0.0.1)
- Forward Port: the app port (e.g., 8123)
- Block Common Exploits: enabled
- Websockets Support: enabled (for apps like Home Assistant, Portainer, or anything real-time)
3) Under the SSL tab, select “Request a new SSL Certificate” and choose Let’s Encrypt. Enable “Force SSL” and “HTTP/2 Support”. NPM will fetch and auto-renew certificates for the hostname.
Step 6 — Enforce Zero Trust access (optional but recommended)
Use Cloudflare Access to protect sensitive apps with identity-aware policies. In Zero Trust → Access → Applications, add an app for app1.example.com, choose “Self-hosted”, and require login with your IdP (Google, GitHub, Microsoft, etc.). You can restrict by email domain, group, or country. Access will challenge users at the edge before requests hit your tunnel.
Step 7 — Security hardening tips
- In Cloudflare DNS, keep your A/AAAA records orange-cloud (proxied).
- Enable WAF and Bot Fight Mode where appropriate.
- For APIs or admin panels, add Access policies and IP allowlists in Cloudflare, and enable “Block Common Exploits” in NPM.
- If you need end-to-end encryption to NPM, set up a trusted origin certificate from Cloudflare and configure NPM to use HTTPS upstreams.
Troubleshooting
502 Bad Gateway: Check the NPM “Forward Hostname/IP” and port. Ensure the target app is reachable from the NPM container network.
403 from Cloudflare Access: Confirm your email is allowed by the Access policy and that your device clock is correct.
WebSockets not working: Enable “Websockets Support” in NPM and verify the app uses the correct path. Most real-time dashboards require this.
Large uploads fail: In NPM, add a Custom Nginx config snippet such as client_max_body_size 100m; for the specific host.
SSL mismatch or loops: Use HTTP between Cloudflare and NPM if Cloudflare terminates TLS at the edge. If you enable origin TLS, make sure certificates and trust are configured properly.
Scaling and operations
- Add more hostnames in the Tunnel as you publish new services; just point them to http://npm:80 and configure the upstream in NPM.
- Use multiple cloudflared instances (on different servers) in the same Tunnel for high availability; Cloudflare will load-balance them.
- Monitor with docker compose logs -f and Cloudflare analytics. Schedule updates with watchtower or perform manual rolling updates.
Wrap-up
You have exposed internal services securely on your own domain without opening any inbound ports. Cloudflare Tunnel handles the edge and connectivity, while Nginx Proxy Manager gives you a clean UI for reverse proxy rules, SSL, and performance tweaks. With Access policies, WAF, and good Nginx hygiene, you can safely run public-facing apps from a single Docker host.
Comments
Post a Comment