Overview
If you want to publish a self‑hosted service on the internet without opening ports on your router, Cloudflare Tunnel is a modern, zero‑trust solution. In this tutorial, you will deploy Cloudflare Tunnel with Docker, route a subdomain to a local container, and add single‑sign‑on (SSO) protection. This setup works well for homelabs and small businesses, is fast to roll out, and uses Cloudflare’s free plan.
Prerequisites
1) A domain added to Cloudflare (DNS managed by Cloudflare). 2) A Linux server or VM with internet access. 3) Docker and the Docker Compose plugin installed. 4) Basic command‑line access via SSH.
Step 1 — Install Docker and Compose
On Debian/Ubuntu, you can install from the OS repo for a quick start. For production, prefer Docker’s official repositories. Quick start example:
sudo apt update && sudo apt install -y docker.io docker-compose-plugin
Verify installation:
docker --version
docker compose version
Step 2 — Prepare a working directory
Create a project folder to keep tunnel files and your Compose file organized:
mkdir -p ~/cf-tunnel/cloudflared && cd ~/cf-tunnel
Step 3 — Create the Tunnel in Cloudflare
Open Cloudflare Dashboard > Zero Trust > Networks > Tunnels > Add a tunnel. Choose “Cloudflared” and give it a friendly name (for example, homelab-tunnel). After creation, click the tunnel and add a “Public Hostname.” Set Hostname to a subdomain like app.yourdomain.com, and Type to HTTP. For the service URL, use a local address you will run (for example, http://app:3000). Saving this will also create the DNS CNAME for you automatically.
In the same page, download the credentials file (a JSON file with your tunnel ID) and, if offered, the suggested config.yml. Save the JSON to ~/cf-tunnel/cloudflared/ and note the filename; it looks like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json.
Step 4 — Create cloudflared config.yml
If you did not download a config from the dashboard, create one now at ~/cf-tunnel/cloudflared/config.yml with the following content (replace placeholders):
tunnel: YOUR-TUNNEL-UUID
credentials-file: /etc/cloudflared/YOUR-TUNNEL-UUID.json
ingress:
- hostname: app.yourdomain.com
service: http://app:3000
- service: http_status:404
This tells cloudflared to forward traffic for your subdomain to the local container named app on port 3000, and return a 404 for everything else.
Step 5 — Create a Docker Compose file
We will run a sample application and cloudflared in the same Docker network. Create ~/cf-tunnel/compose.yml with:
services:
app:
image: traefik/whoami:latest
container_name: whoami
expose:
- "80"
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
command: tunnel --config /etc/cloudflared/config.yml run
volumes:
- ./cloudflared:/etc/cloudflared:ro
depends_on:
- app
restart: unless-stopped
The app service is a tiny HTTP server used as a demo. Cloudflared reads your config and credentials from the mounted folder.
Step 6 — Start the stack and test
Run the following from ~/cf-tunnel:
docker compose up -d
docker logs -f cloudflared
When logs show “Connected to Cloudflare,” visit https://app.yourdomain.com. You should see a response from “whoami.” Use curl -I https://app.yourdomain.com to verify status 200 over HTTPS.
Optional — Add Zero Trust access
To protect your app with SSO, open Cloudflare Dashboard > Zero Trust > Access > Applications > Add an application > Self‑hosted. Set the application domain to app.yourdomain.com. Add a policy to “Allow” specific emails, domains, or identity providers (Google, GitHub, Azure AD). Save. Your app now prompts users to authenticate before reaching your origin.
Maintenance and updates
Update images periodically to receive security patches and performance improvements. Use:
docker compose pull && docker compose up -d
Because the tunnel runs outbound over HTTPS, you do not need to open inbound ports on your router. Keep your server’s system packages current and restrict SSH access with keys and a firewall.
Troubleshooting
502 Bad Gateway: Usually means cloudflared cannot reach your container. Confirm the service name and port in config.yml, ensure both containers share the same Docker network (Compose sets this by default), and that the app is listening on the correct port.
404 Not Found: If you have multiple hostnames, make sure the correct hostname entry exists in the ingress block and that it points to the right service. The last rule should be a 404 fallback.
DNS not resolving: In the Tunnel page, verify the “Public Hostname” exists and the DNS CNAME was created. If needed, create a CNAME for app.yourdomain.com pointing to YOUR-TUNNEL-UUID.cfargotunnel.com.
Authentication loop with Access: Clear cookies or add your domain to “Allowed Cookie Domains” in the Access app settings. Also confirm your policy “Allow” rules match your user identity.
Connectivity drops: Check server time (NTP), ensure no outbound firewall is blocking HTTPS to Cloudflare, and consider running two cloudflared replicas for high availability.
What you achieved
You deployed a secure reverse tunnel with Docker that exposes a local container to the internet under your own domain, without port forwarding or a public IP. You also learned how to enable Cloudflare Access as a zero‑trust layer for SSO and policy control. This pattern scales to multiple services by adding more ingress rules or additional public hostnames in the dashboard, making it a clean, maintainable approach to self‑hosting.
Comments
Post a Comment