🤔 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:
- Open the port in the firewall (UFW, iptables…)
- Configure a reverse proxy (Nginx, Caddy…)
- Obtain an SSL certificate (Let's Encrypt…)
- Manage the renewal of the certificate
- 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:
cloudflared(the tunnel client) runs on your VPS- It establishes an outbound connection to Cloudflare
- Cloudflare routes incoming traffic to your tunnel
- Your service remains on
localhost— no 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.
cloudflaredconsumes 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 addnoTLSVerify: trueif the service is using local HTTPS. - ERR_NAME_NOT_RESOLVED → DNS is not configured. Check with
digand reruncloudflared 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 listand 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
- The problem: exposing a service without putting yourself at risk
- The solution: Cloudflare Tunnel
- Prerequisites
- Step 1: Install cloudflared
- Step 2: Authentication
- Step 3: Create the tunnel
- Step 4: Configure the tunnel
- Step 5: Configure DNS
- Step 6: Start the tunnel
- Use case 1: Expose a FastAPI API
- Use case 2: Protected admin dashboard
- Use case 3: Webhook receiver
- Advanced security: lock down your VPS
- Monitoring and debugging
- Advanced configuration
- Advanced troubleshooting
- Summary: the complete checklist
- Tips and best practices
- Cloudflare Tunnel vs alternatives
- Common errors
- Recommended tools
- FAQ
- Conclusion
📋 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 Trust → Networks → Tunnels, 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
ingressrules per hostname - In production, install
cloudflaredas 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:404without a hostname, otherwise the configuration is rejected - Wrong port in the service: a
502 Bad Gatewayalmost always means that the application is not listening on the port specified inconfig.yml - Not copying the credentials: after
sudo cloudflared service install, the service reads the config from/etc/cloudflared/— remember to copy yourconfig.ymland the JSON file there - Local HTTPS without
noTLSVerify: if your local service uses HTTPS, addnoTLSVerify: trueinoriginRequestotherwise cloudflared will refuse the connection - Unrouted DNS: if you get
ERR_NAME_NOT_RESOLVED, it means thecloudflared tunnel route dnscommand was not executed for the specified hostname
🔧 Recommended tools
- 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.
```