📑 Table of contents

Cloudflare Tunnel: Expose your services without opening ports

Cloudflare Tunnel: Expose your services without opening ports

Self-Hosting 🟡 Intermediate ⏱️ 14 min read 📅 2026-02-24

🤔 The problem: exposing a service without putting yourself at risk

When you run an application on a VPS (a FastAPI API on port 8000, an admin dashboard on port 3000, a webhook on port 5000…), it is only accessible locally.

To make it accessible from the Internet, the classic method is to:

  1. Open the port in the firewall (UFW, iptables…)
  2. Configure a reverse proxy (Nginx, Caddy…)
  3. Obtain an SSL certificate (Let's Encrypt…)
  4. Manage the renewal of the certificate
  5. Protect yourself against DDoS attacks

Each step adds complexity and risks:

Risk Description
Open ports Every open port is a potential attack surface
Bad Nginx config Missing headers, poorly configured TLS, server info leak
Expired certificate Inaccessible service, browser errors
DDoS A standard VPS cannot withstand a volumetric attack
Port scanning Bots are constantly scanning public IPs

What if you could avoid all of this?

🚀 The solution: Cloudflare Tunnel (formerly Argo Tunnel)

Cloudflare Tunnel creates an outbound connection from your server to the Cloudflare network. No inbound ports are required.

⚙️ How it works

[User]  [Cloudflare Edge]  encrypted tunnel  [cloudflared on your VPS]  [localhost:8000]

The traffic flow is reversed compared to a traditional server:

  1. cloudflared (the tunnel client) runs on your VPS
  2. It establishes an outbound connection to Cloudflare
  3. Cloudflare routes incoming traffic to your tunnel
  4. Your service remains on localhostno open ports

✅ Advantages

Feature Traditional (Nginx + Let's Encrypt) Cloudflare Tunnel
Ports to open 80, 443 + service ports None (0)
SSL Let's Encrypt (manual config) Automatic
DDoS Protection None (or paid) Included for free
Reverse proxy Nginx/Caddy to configure Built-in
Difficulty Intermediate to advanced Beginner to intermediate
Cost Free Free
Zero Trust No Yes (Access policies)

🛠️ Prerequisites

Before you begin, you will need:

  • A VPS with Linux (Ubuntu/Debian recommended) — Hostinger offers excellent VPS starting at €3.99/month with a 20% discount
  • A domain name pointed to Cloudflare (Cloudflare nameservers)
  • A Cloudflare account (free)
  • A local service that is running (API, website, dashboard…)

💡 VPS Tip: For a Cloudflare tunnel, even a small VPS is sufficient. cloudflared consumes very few resources (~20 MB of RAM). Hostinger is ideal for getting started with an excellent price-to-performance ratio.

📥 Step 1: Install cloudflared

🐧 On Ubuntu/Debian

Installation on Ubuntu/Debian is done via the official Cloudflare repository: we add the GPG key, then the repository, and install the cloudflared package with apt. This method guarantees automatic updates.

✔️ Verify the installation

cloudflared --version

📦 Alternative: direct binary

If the repository causes issues, you can download the official binary from the GitHub releases and place it in /usr/local/bin/ with execution rights.

🔐 Step 2: Authentication

Log in to your Cloudflare account by running the cloudflared tunnel login command. This command opens a link in the terminal: copy it into your browser, select the domain to use, and authorize. A certificate will be saved in ~/.cloudflared/cert.pem. You can verify its presence with the command ls -la ~/.cloudflared/cert.pem.

🏗️ Step 3: Create the tunnel

Run the command cloudflared tunnel create mon-tunnel. It generates a unique UUID for the tunnel and saves the credentials in a JSON file. Note this UUID, you will need it for the configuration. Then list your tunnels with cloudflared tunnel list to verify that everything is in order.

⚙️ Step 4: Configure the tunnel

Create the configuration file using the command nano ~/.cloudflared/config.yml.

📝 Simple configuration (single service)

tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /root/.cloudflared/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json

ingress:
  - hostname: api.mondomaine.com
    service: http://localhost:8000
  - service: http_status:404

🔀 Multi-service configuration

For multiple services, add an ingress entry per hostname, each pointing to a different local port. You can also add originRequest options like connectTimeout or noTLSVerify depending on the needs of each service. The last entry must always be a catch-all http_status:404 without a hostname — this is mandatory.

✅ Validate the configuration

cloudflared tunnel ingress validate

🌐 Step 5: Configure DNS

For each hostname in your config, automatically create a DNS CNAME record using the command cloudflared tunnel route dns mon-tunnel api.mondomaine.com (repeat for each subdomain). This automatically creates CNAME records in your Cloudflare zone, all pointing to your tunnel's cfargotunnel.com address with the proxy enabled.

▶️ Step 6: Run the tunnel

🧪 Manual test

Run the tunnel using the command cloudflared tunnel run mon-tunnel. If the tunnel is operational, you will see 4 connections registered in the logs. Then test in your browser: https://api.mondomaine.com

🏭 As a systemd service (production)

To make the tunnel start automatically at boot, install it as a systemd service via sudo cloudflared service install. The service uses the config located in /etc/cloudflared/config.yml — remember to copy your configuration file and your JSON credentials there. Then enable it with sudo systemctl enable cloudflared.

🐍 Use Case 1: Exposing a FastAPI API

Let's imagine you are developing an AI API with FastAPI. The application exposes a /health endpoint for monitoring and a /generate endpoint for AI requests, accepting a prompt and a max_tokens parameter.

Start the API on localhost with the command uvicorn api:app --host 127.0.0.1 --port 8000.

Tunnel configuration adapted for AI services:

ingress:
  - hostname: api.mondomaine.com
    service: http://localhost:8000
    originRequest:
      connectTimeout: 30s  # Longer for AI requests
  - service: http_status:404

Your API is now accessible at https://api.mondomaine.com with automatic SSL! Test it with a curl to /health or /generate from anywhere.

📊 Use case 2: Protected admin dashboard

For an admin dashboard (Streamlit, Grafana, or custom), you want an additional authentication layer.

Cloudflare Access (Zero Trust)

Cloudflare offers an authentication system for free (up to 50 users in 2025). From the Zero Trust → Access → Applications dashboard, create a Self-hosted application, specify the subdomain admin.mondomaine.com, and define a policy allowing only your email.

Now, when someone accesses admin.mondomaine.com, they must first authenticate via Cloudflare Access. Even if your dashboard doesn't have a login, it is protected.

# config.yml - the dashboard is on localhost:8501 (Streamlit)
ingress:
  - hostname: admin.mondomaine.com
    service: http://localhost:8501
    originRequest:
      noTLSVerify: true
  - service: http_status:404

Verify the JWT header in your app

Cloudflare Access adds a Cf-Access-Jwt-Assertion header to every request. You can verify it on the server side by retrieving the token, downloading the public keys from your Cloudflare team's /cdn-cgi/access/certs endpoint, and then decoding the JWT with the RS256 algorithm while verifying the audience matching your URL.

🔗 Use case 3: Webhook receiver

Webhooks (GitHub, Stripe, Telegram…) require a public HTTPS URL. Cloudflare Tunnel is perfect for this. On the application side, you just need to retrieve the request body, verify the signature (for example X-Hub-Signature-256 for GitHub with HMAC-SHA256), and then process the event.

ingress:
  - hostname: webhook.mondomaine.com
    service: http://localhost:5000
    originRequest:
      connectTimeout: 10s
  - service: http_status:404

🔒 Advanced security: locking down your VPS

Now that everything goes through Cloudflare Tunnel, you can close all ports except SSH using UFW: deny all incoming traffic by default, allow outgoing traffic, open only port 22 in TCP, then enable the firewall.

Your VPS now has only a single open port: SSH. Everything else goes through the tunnel.

Going further: restrict SSH too

You can restrict SSH to your IP only with sudo ufw allow from VOTRE_IP to any port 22 proto tcp, or even route SSH through the Cloudflare tunnel by adding an ssh://localhost:22 rule in your ingress config. On the client side, configure your ~/.ssh/config with ProxyCommand cloudflared access ssh --hostname %h to connect via the tunnel.

🔍 Monitoring and debugging

Check tunnel status

Use sudo systemctl status cloudflared for the service status, sudo journalctl -u cloudflared -f for real-time logs, and cloudflared tunnel info mon-tunnel for detailed metrics.

Detailed logs

To start the tunnel in debug mode, add the --loglevel debug option or add loglevel: debug in your config.yml.

🔧 Advanced configuration

Timeouts and performance

Adjust the timeouts according to the service type in the originRequest section of each ingress rule. For example: 10 seconds for a standard API, 120 seconds for an AI service with slow responses or streaming, 5 seconds for static files. WebSockets are natively supported without additional configuration.

Custom headers

Cloudflare automatically adds useful headers to each request: Cf-Connecting-Ip (visitor's real IP), X-Forwarded-For, and X-Forwarded-Proto. On the application side, you can retrieve the visitor's real IP and country directly from these headers.

Metrics and observability

Enable Prometheus metrics by adding metrics: localhost:2000 at the root level of your config. You will then get metrics such as the total number of requests, errors, concurrent requests, responses by HTTP code, and latency.

🐛 Advanced Troubleshooting

Step-by-step diagnostics

When something isn't working, follow this decision tree:

  • 502 Bad Gateway → Check that the local service is running (curl http://localhost:PORT), that the port is correct in config.yml, and add noTLSVerify: true if the service is using local HTTPS.
  • ERR_NAME_NOT_RESOLVED → DNS is not configured. Check with dig and rerun cloudflared tunnel route dns.
  • ERR_CONNECTION_TIMED_OUT → The tunnel is not running, the credentials are invalid, or the firewall is blocking outgoing traffic to *.cloudflare.com.
  • 1033: Argo Tunnel error → The tunnel no longer exists. Check with cloudflared tunnel list and recreate it if necessary.
  • Access denied (with Cloudflare Access) → Check that your email is in the policy and that the cookie has not expired.

Useful diagnostic commands

Essential commands for diagnosing: cloudflared tunnel run --loglevel debug for connection logs, dig +short to check DNS, curl -v to test the local service, ss -tlnp for active connections, journalctl for system logs, and cloudflared tunnel ingress validate to validate the config.

Common problems and solutions

The tunnel disconnects regularly

Check network stability with an extended ping. The systemd service automatically restarts the tunnel in case of a crash. For extra safety, you can add a cron monitoring script (every 5 minutes) that checks the service status and restarts it if necessary, with an optional alert via Telegram.

High latency on requests

Check the Cloudflare datacenter being used with curl -s https://api.mondomaine.com/cdn-cgi/trace | grep colo. If the datacenter is geographically distant, it's a Cloudflare routing issue.

Migrating to a new server

To migrate, simply copy the cert.pem, the credentials JSON files, and the config.yml to the new server in ~/.cloudflared/. Install the service with sudo cloudflared service install and start it. No DNS changes are necessary — the tunnel automatically resumes on the new server.

📋 Table of Contents

📋 Summary: the complete checklist

  • ✅ 1. VPS with Linux (Hostinger recommended)
  • ✅ 2. Domain pointed to Cloudflare (nameservers)
  • ✅ 3. cloudflared installed
  • ✅ 4. Authentication (cloudflared tunnel login)
  • ✅ 5. Tunnel created (cloudflared tunnel create)
  • ✅ 6. config.yml written
  • ✅ 7. DNS configured (cloudflared tunnel route dns)
  • ✅ 8. Successful manual test
  • ✅ 9. systemd service installed and enabled
  • ✅ 10. Firewall locked down (UFW)
  • ✅ 11. Cloudflare Access configured (optional)

💡 Tips and best practices

1. Name your tunnels intelligently

Prefer explicit names like prod-api-fastapi or staging-dashboard rather than tunnel1.

2. One tunnel per environment

Separate production and staging with distinct tunnels to isolate environments.

3. Use environment variables

Avoid hardcoding UUIDs in your scripts — use environment variables for the tunnel ID and the credentials path.

4. Monitor your tunnels

In the Cloudflare dashboard → Zero TrustNetworksTunnels, you can see the status of each tunnel (Healthy/Degraded/Down), active connections, and real-time traffic.

5. Backup your credentials

The cert.pem files and credentials JSONs are critical. Without them, you will have to recreate the tunnel. Make regular backups.

🆚 Cloudflare Tunnel vs alternatives

Solution Open ports Auto SSL DDoS protection Cost Difficulty
Cloudflare Tunnel ❌ 0 Free ⭐⭐
Nginx + Let's Encrypt ✅ 80, 443 Free ⭐⭐⭐
Caddy ✅ 80, 443 Free ⭐⭐
ngrok ❌ 0 Freemium
Tailscale Funnel ❌ 0 Freemium ⭐⭐

Cloudflare Tunnel wins on almost all criteria for production use.

🎯 Key takeaways

  • Cloudflare Tunnel creates an encrypted outbound connection between your VPS and Cloudflare — no inbound ports to open
  • Automatic SSL and DDoS protection are included for free
  • The configuration fits in a single YAML file with ingress rules per hostname
  • In production, install cloudflared as a systemd service for automatic restarts
  • Once the tunnel is active, close all firewall ports except SSH
  • Cloudflare Access (Zero Trust) adds a free authentication layer for your sensitive dashboards

❌ Common mistakes

  • Forgetting the catch-all: the last ingress rule must always be service: http_status:404 without a hostname, otherwise the configuration is rejected
  • Wrong port in the service: a 502 Bad Gateway almost always means that the application is not listening on the port specified in config.yml
  • Not copying the credentials: after sudo cloudflared service install, the service reads the config from /etc/cloudflared/ — remember to copy your config.yml and the JSON file there
  • Local HTTPS without noTLSVerify: if your local service uses HTTPS, add noTLSVerify: true in originRequest otherwise cloudflared will refuse the connection
  • Unrouted DNS: if you get ERR_NAME_NOT_RESOLVED, it means the cloudflared tunnel route dns command was not executed for the specified hostname
  • cloudflared: the official Cloudflare Tunnel client (free, ~20 MB of RAM)
  • UFW: simplified firewall to lock down your VPS after setting up the tunnel
  • systemd: service manager to ensure cloudflared restarts automatically
  • Cloudflare Zero Trust Dashboard: web interface to configure Access policies, monitor tunnels, and manage users
  • Hostinger: recommended VPS hosting provider for this setup, with an excellent price-to-performance ratio starting at €3.99/month

❓ FAQ

Is Cloudflare Tunnel really free?
Yes, the free plan includes unlimited tunnels, automatic SSL, and DDoS protection. The only limit is on Cloudflare Access (50 free users).

Can I use Cloudflare Tunnel without a domain name?
No, a domain configured with Cloudflare nameservers is required to create the CNAME records to the tunnel.

Does the tunnel add latency?
Very little. The additional latency is generally under 10 ms if the nearest Cloudflare datacenter is well routed. Check with the /cdn-cgi/trace endpoint.

What happens if cloudflared crashes?
The systemd service automatically restarts the process. For additional monitoring, add a healthcheck script in cron.

Can I expose SSH via the tunnel?
Yes, with an ssh://localhost:22 rule in the ingress and ProxyCommand cloudflared access ssh --hostname %h on the client side. This allows you to close port 22 in the firewall.

🎯 Conclusion

Cloudflare Tunnel radically transforms the way you expose services:

  • Zero open ports = minimal attack surface
  • Automatic SSL = never deal with expired certificates again
  • Free DDoS protection = peace of mind
  • Simple configuration = just one YAML file and you're good to go
  • Zero Trust = modern authentication included

Combined with a Hostinger VPS (performant and affordable), you have a solid production infrastructure for just a few euros a month. If you want to take your self-hosting of AI services further, check out our guide VPS + AI: the complete setup to self-host everything. To optimize your API calls from your exposed services, our comparison AI APIs: OpenRouter vs direct calls will help you choose the right approach. And if you containerize your services, Docker + AI: containerizing your smart services perfectly completes this setup.
```