Skip Nginx and firewall configs — automatic SSL, DNS routing, and systemd setup

This week, I finally replaced all of that with Cloudflare Tunnel.No Nginx. No Let’s Encrypt. No open ports. Just a persistent, encrypted connection from your server to Cloudflare’s network, automatically routing your container to a public subdomain. In this guide, I’ll show you exactly how to do it—using n8n as an example, though it works for any container running on any port. 1. The Traditional Setup (and Why It’s Painful)Normally, exposing a containerized app to the public internet means: - Opening ports 80 and 443 on your server
- Setting up Nginx as a reverse proxy
- Managing SSL certificates via Let’s Encrypt
- Manually adding a DNS record in Cloudflare or your registrar
2. What Cloudflare Tunnel Does DifferentlyCloudflare Tunnel flips the model. Instead of exposing ports, your server makes a secure outbound connection to Cloudflare.Cloudflare then handles HTTPS, certificates, and routing for you—essentially becoming your reverse proxy at the edge. The benefits: - No public ports
- No Nginx
- Automatic SSL
- Built-in DDoS and access control through Cloudflare
All you need is the Cloudflare account that manages your domain and a small CLI agent (cloudflared).
3. PrerequisitesMake sure you have: - A Cloudflare-managed domain (e.g., example.com)
- A server (Ubuntu 22.04 or later)
- Docker installed
- Cloudflare Tunnel CLI (cloudflared)
Install cloudflared for Ubuntu 22.04 (Jammy → Bullseye base): sudo mkdir -p --mode=0755 /usr/share/keyringscurl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/nullecho 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflared.listsudo apt-get update && sudo apt-get install cloudflared -y
4. Run Any Container LocallyFor the example, let’s use n8n, which runs on port 5678: docker volume create n8n_datadocker run -d \ --name n8n \ -p 5678:5678 \ -e GENERIC_TIMEZONE="CET" \ -e TZ="CET" \ -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \ -e N8N_RUNNERS_ENABLED=true \ -e WEBHOOK_URL="https://app.example.com/" \ -v n8n_data:/home/node/.n8n \ docker.n8n.io/n8nio/n8nYou can replace this with any container that listens on a port—Grafana (3000), a Node app (8080), etc.
5. Authenticate Cloudflare TunnelFirst, log in to Cloudflare from your server: cloudflared loginThis opens your browser, asks you to pick your domain, and then saves credentials to: ~/.cloudflared/cert.pem
6. Create a Named TunnelNow create a tunnel and name it (for example, docker-tunnel): cloudflared tunnel create docker-tunnelYou’ll see output like: Created tunnel docker-tunnel with id c101ebf4-e2d3-49b9-8735-c0e48ebf6a56This also generates a credentials file, usually at: ~/.cloudflared/<tunnel-id>.json
7. Route a Subdomain to the TunnelYou can create a public subdomain in Cloudflare automatically: cloudflared tunnel route dns docker-tunnel app.example.comCloudflare adds a CNAME record pointing to the tunnel—no manual DNS setup required.
8. Run the Tunnel and Map It to the ContainerLet’s connect the subdomain to your local port: cloudflared tunnel --url http://localhost:5678 run docker-tunnelYou’ll see Cloudflare establish several secure connections to its edge servers.After a few seconds, your app will be available at: https://app.example.com
9. Make It Persistent with systemdTo keep the tunnel alive after you close the SSH session (and on reboot), use a config file and a systemd service. Create Config FileFile: ~/.cloudflared/config.yml tunnel: docker-tunnelcredentials-file: /home/matija/.cloudflared/c101ebf4-e2d3-49b9-8735-c0e48ebf6a56.jsoningress: - hostname: app.example.com service: http://localhost:5678 - service: http_status:404Create a systemd ServiceFile: /etc/systemd/system/cloudflared-docker.service [Unit]Description=Cloudflare Tunnel for Docker AppAfter=network.target[Service]User=matijaExecStart=/usr/bin/cloudflared --config /home/matija/.cloudflared/config.yml tunnel run docker-tunnelRestart=on-failureRestartSec=5[Install]WantedBy=multi-user.targetEnable and start it: sudo systemctl daemon-reloadsudo systemctl enable --now cloudflared-docker.serviceCheck it’s active: sudo systemctl status cloudflared-dockerNow your tunnel reconnects automatically after reboot or crash.
10. Verify and TestVisit your domain: https://app.example.comCheck Cloudflare DNS — you’ll see a CNAME like: app.example.com → <tunnel-id>.cfargotunnel.comNo ports are open on your server.Cloudflare terminates HTTPS and securely forwards traffic to your container.
11. (Optional) Add Cloudflare AccessFor production, you can protect your exposed container with Cloudflare Access (Zero Trust).From the Cloudflare dashboard: - Go to Zero Trust → Access → Applications
- Add your subdomain (e.g., app.example.com)
- Require Google, GitHub, or email login before granting access
You now have SSO-level protection in front of any local app—no code changes required.
12. ConclusionInstead of juggling Nginx configs, DNS records, and SSL renewals, Cloudflare Tunnel gives you a single, elegant workflow: - Run your Docker container locally
- Connect it securely to Cloudflare
- Let Cloudflare handle HTTPS and DNS automatically
- Keep it persistent with a systemd service
This setup is fast, secure, and production-ready—and it works for any container on any port. If you've been doing the old Nginx + Certbot dance for every new side project, this will feel like cheating (in the best way possible). For migrating existing Docker setups, check out my guide on migrating Docker containers between VPS.
By: BuildWithMatija 26th December 2025
|