How to Set Up Cloudflare Tunnel on Ubuntu 24.04 VPS: Expose Services Without Port Forwarding
Exposing a self-hosted service to the public internet traditionally means opening inbound ports on your firewall, configuring NAT rules on your router, dealing with dynamic IP addresses, and hoping your VPS provider does not rate-limit you when a bot starts probing port 22. Cloudflare Tunnel removes all of that. Instead of listening for incoming traffic, your server opens an outbound connection to Cloudflare's edge and receives requests through that persistent tunnel. This guide walks you through installing cloudflared on an Ubuntu 24.04 VPS, creating a tunnel, routing a hostname to it, hardening it with Zero Trust access policies, and running it as a reliable systemd service.
Looking for a lean, stable VPS to run cloudflared on? The CloudCore Starter plan has plenty of headroom for one or more tunnels plus the services behind them.
Table of Contents
What is Cloudflare Tunnel?
Cloudflare Tunnel is a free service that lets you publish internal services to the internet without opening inbound ports or exposing your VPS IP. You install a small daemon called cloudflared on the origin server, it dials out to Cloudflare's edge, and that outbound connection becomes the transport for every request to your public hostname. From the outside, app.example.com resolves to Cloudflare; traffic reaches your server via the tunnel; no one on the public internet can connect to your origin directly.
Cloudflare Tunnel is part of the broader Cloudflare One / Zero Trust platform. You can expose HTTP applications, raw TCP services (SSH, RDP, databases), and even entire private networks to remote devices running the WARP client. Common deployment patterns include publishing a self-hosted app like Nextcloud or Ghost, giving your team SSH access to a VPS without opening port 22, reaching a home lab that sits behind CGNAT, and building site-to-site networks without static IPs or IPsec tunnels.
Under the hood, cloudflared multiplexes traffic over HTTP/2 or QUIC to the nearest Cloudflare data center. It establishes multiple redundant connections so that if one Cloudflare edge location has a hiccup, traffic fails over transparently. Because the TLS certificate is managed by Cloudflare at the edge, your origin never needs a public certificate — cloudflared can talk to your local service over plain HTTP on 127.0.0.1, and the TLS handshake with the end user happens entirely at the edge.
Why Use Cloudflare Tunnel Instead of Port Forwarding?
Compared to the traditional "open port 443, point DNS at your IP, install Let's Encrypt" workflow, Cloudflare Tunnel offers several concrete benefits:
- No inbound ports — Your VPS firewall can deny every inbound connection on 80/443. Attackers cannot port-scan your origin because your origin is not advertising any listening socket to the public internet.
- Works behind NAT and CGNAT — No public IPv4? No problem. The tunnel is established outbound, so as long as your server can reach the internet on port 443, it can publish services. This is the cleanest way to expose services on a home lab or a mobile hotspot.
- Automatic DDoS protection — All traffic passes through Cloudflare's edge first. Volumetric attacks are absorbed by Cloudflare's anycast network before they ever reach your origin.
- Free TLS at the edge — Cloudflare issues and renews certificates for you. You never run certbot again for tunneled hostnames.
- IP rotation is harmless — If your VPS ever changes IP addresses, nothing breaks. The tunnel reconnects automatically and DNS records are not tied to the origin IP.
- Generous free tier — Cloudflare Tunnel and Cloudflare Access (up to 50 users) are free for personal and small business use. There are no per-seat costs until you scale into Zero Trust Enterprise.
- Single pane for access policies — Cloudflare Access lets you put authentication in front of any tunneled hostname without modifying the underlying app. Add Google SSO, Okta, email-OTP, or device posture checks from the dashboard.
Cloudflare Tunnel vs. Alternatives
| Feature | Port Forward + Certbot | Reverse SSH Tunnel | Tailscale Funnel | Cloudflare Tunnel |
|---|---|---|---|---|
| Public inbound ports required | Yes (80/443) | Yes (jump host) | No | No |
| Works behind CGNAT | No | Yes | Yes | Yes |
| DDoS protection | DIY | No | No | Yes (built-in) |
| TLS management | Manual (certbot) | Manual | Automatic | Automatic |
| Zero Trust access policies | No (build yourself) | No | Limited | Yes (free 50 users) |
| Custom domains | Yes | Yes | Limited | Yes |
| Monthly cost (1 service) | Free | Free | Free tier | Free |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- A Cloudflare account (free plan is fine) with a domain added and nameservers pointed at Cloudflare — if you have not done this yet, see developers.cloudflare.com/fundamentals/setup
- A service running locally on the VPS that you want to expose — for this guide we assume something on
http://localhost:8080, but the same steps work for any port and protocol
Recommended Plan: CloudCore Starter>
cloudflared itself is extremely lightweight (typically 40-80 MB RAM at idle). What matters is the service behind it. For a handful of self-hosted apps — Ghost, Nextcloud, Grafana, a static site — the CloudCore Starter plan gives you plenty of room:
>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth at 1 Gbps>
Because all ingress goes through Cloudflare's edge, you can also lock the VPS firewall down to outbound-only, which is a huge security win regardless of the plan you choose.
Connect to your server:
ssh root@your-server-ipStep 1: Update System Packages
Refresh your package index and apply any pending upgrades before installing new software:
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.If the kernel was updated, reboot before continuing:
sudo rebootStep 2: Install cloudflared from the Official Apt Repo
Cloudflare publishes cloudflared through its own apt repository, which means you get automatic updates via apt upgrade just like any other system package. This is the recommended install method — avoid the one-shot .deb download unless you have a specific reason, because it will not auto-update.
Add Cloudflare's GPG key:
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/nullAdd the repository. Ubuntu 24.04 is codename noble:
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared noble main" | sudo tee /etc/apt/sources.list.d/cloudflared.listUpdate the package index and install:
sudo apt update
sudo apt install -y cloudflaredVerify the install:
cloudflared --versionExpected output:
cloudflared version 2025.2.1 (built 2025-02-18-1045 UTC)The binary lives at /usr/local/bin/cloudflared (or /usr/bin/cloudflared depending on version). You can run it as any user at this point — no daemon is running yet.
Step 3: Authenticate cloudflared with Your Cloudflare Account
Before you can create a tunnel bound to your domain, cloudflared needs a certificate that proves it is authorized to manage DNS for your zone. You obtain this by logging in through a browser.
Run the login command:
cloudflared tunnel loginExpected output:
Please open the following URL and log in with your Cloudflare account:https://dash.cloudflare.com/argotunnel?aud=&callback=https%3A%2F%2Flogin.cloudflareaccess.org%2F...
Leaving cloudflared running to download cert automatically.
Copy the URL into your local browser (not the VPS's browser), log in to Cloudflare, and pick the zone (domain) you want this tunnel to serve. Cloudflare then writes a certificate file called cert.pem into ~/.cloudflared/ on the VPS.
When it is done you will see:
You have successfully logged in.
If you wish to copy your credentials to a server, they have been saved to:
/root/.cloudflared/cert.pemThis cert is what authorizes cloudflared to create DNS records and new tunnels in that zone. Treat it like an API key — anyone with this file can create tunnels on your account.
Step 4: Create a Tunnel
Each tunnel is a named logical connection between your origin and Cloudflare's edge. A single tunnel can serve many hostnames via ingress rules, so you usually want one tunnel per server rather than one per app.
Create a tunnel named my-tunnel:
cloudflared tunnel create my-tunnelExpected output:
Tunnel credentials written to /root/.cloudflared/6f2a8b91-c3d4-4e2f-9a1b-0c8d7e6f5a4b.json. cloudflared chose to use credentials file instead of token. Keep this file secret. To revoke these credentials, delete the tunnel.
Created tunnel my-tunnel with id 6f2a8b91-c3d4-4e2f-9a1b-0c8d7e6f5a4b
Two things just happened:
~/.cloudflared/<uuid>.json on your VPS. This file contains the tunnel secret — the runtime credential cloudflared uses to connect. It is separate from the cert.pem from Step 3.List your tunnels to confirm:
cloudflared tunnel listExpected output:
ID NAME CREATED CONNECTIONS
6f2a8b91-c3d4-4e2f-9a1b-0c8d7e6f5a4b my-tunnel 2026-04-16T10:00:00ZWrite the UUID down — you will reference it in the config file in the next step.
Step 5: Configure Ingress Rules
The config file at /etc/cloudflared/config.yml tells cloudflared which hostnames to accept and where to send the traffic locally. Create the directory and file:
sudo mkdir -p /etc/cloudflared sudo tee /etc/cloudflared/config.yml > /dev/null <<'EOF' tunnel: 6f2a8b91-c3d4-4e2f-9a1b-0c8d7e6f5a4b credentials-file: /root/.cloudflared/6f2a8b91-c3d4-4e2f-9a1b-0c8d7e6f5a4b.json
ingress: - hostname: app.example.com service: http://localhost:8080 - hostname: grafana.example.com service: http://localhost:3000 - hostname: ssh.example.com service: ssh://localhost:22 - service: http_status:404 EOF
Replace the UUID, hostnames, and service URLs with your own. A few things to know:
tunnel— The UUID of the tunnel you created in Step 4.credentials-file— Absolute path to the JSON filecloudflared tunnel creategenerated. When you move to running as a systemd service later, this path must be readable by thecloudflaredsystem user.- Ingress rules are evaluated top-to-bottom. The first matching hostname wins. Requests for hosts not matched fall through to the catch-all at the bottom.
- The last rule must be a catch-all with no
hostname:key. Without it,cloudflaredrefuses to start.http_status:404is the standard choice — return a 404 for unknown hostnames. - Service types include
http://,https://,tcp://,ssh://,rdp://,unix:/path/to/socket, andhello_world(a built-in test endpoint).
cloudflared tunnel ingress validateExpected output:
Validating rules from /etc/cloudflared/config.yml
OKTest a specific hostname against your rules:
cloudflared tunnel ingress rule https://app.example.com/healthExpected output:
Using rules from /etc/cloudflared/config.yml
Matched rule #0
hostname: app.example.com
service: http://localhost:8080Step 6: Route DNS to the Tunnel
A tunnel with no DNS record is invisible — nothing on the public internet knows to send app.example.com to Cloudflare's edge. The tunnel route dns command creates a proxied CNAME pointing at <tunnel-uuid>.cfargotunnel.com in one shot:
cloudflared tunnel route dns my-tunnel app.example.com
cloudflared tunnel route dns my-tunnel grafana.example.com
cloudflared tunnel route dns my-tunnel ssh.example.comExpected output for each:
2026-04-16T10:10:00Z INF Added CNAME app.example.com which will route to this tunnel tunnelID=6f2a8b91-...Open the Cloudflare dashboard's DNS tab for your zone and you will see new orange-cloud CNAME records, one per hostname, all pointing at the same cfargotunnel.com target.
If a DNS record for that hostname already exists (for example, an A record from a previous setup), the command refuses to overwrite it. Delete the old record in the dashboard first, then rerun the route command. This conflict is one of the most common beginner gotchas — watch for a record already exists error message.
Step 7: Install cloudflared as a systemd Service
Running cloudflared tunnel run my-tunnel in the foreground works for testing, but for production you want a systemd unit that auto-starts on boot and restarts on failure. cloudflared ships with a helper that sets this up:
sudo cloudflared service installExpected output:
2026-04-16T10:15:00Z INF Using Systemd
2026-04-16T10:15:00Z INF cloudflared service is installedUnder the hood this:
/etc/cloudflared/config.yml and the credentials JSON into /etc/cloudflared/ owned by a cloudflared system user./etc/systemd/system/cloudflared.service.Verify:
sudo systemctl status cloudflaredExpected output:
● cloudflared.service - cloudflared
Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-16 10:15:30 UTC; 10s ago
Main PID: 2345 (cloudflared)
Tasks: 9 (limit: 4552)
Memory: 48.2M
CGroup: /system.slice/cloudflared.service
└─2345 /usr/local/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel runTail the logs while the tunnel connects:
sudo journalctl -u cloudflared -fYou are looking for four Registered tunnel connection lines, one per Cloudflare data center the tunnel has connected to:
INF Registered tunnel connection connIndex=0 location=FRA
INF Registered tunnel connection connIndex=1 location=FRA
INF Registered tunnel connection connIndex=2 location=AMS
INF Registered tunnel connection connIndex=3 location=AMSFour connections means the tunnel is fully redundant. From another machine, browse to https://app.example.com and you should see the service that is running on localhost:8080 on your VPS, served through Cloudflare with a valid TLS certificate.
At this point you can (and should) close inbound 80 and 443 on your firewall:
sudo ufw default deny incoming
sudo ufw allow OpenSSH # keep SSH while you test, consider tunneling SSH too
sudo ufw enableStep 8: Protect the Tunnel with Cloudflare Access (Zero Trust)
Exposing a service via tunnel still makes it publicly reachable by default — anyone with the URL can hit it. Cloudflare Access sits in front of any tunneled hostname and enforces authentication before traffic even reaches your origin.
From the Cloudflare Zero Trust dashboard, go to Access → Applications → Add an application → Self-hosted. Key fields:
- Application name — any label, e.g.
grafana. - Session duration — how long a successful login stays valid (24 hours is a common default).
- Application domain — the exact hostname you want to protect, e.g.
grafana.example.com.
- Emails —
Includea literal list of email addresses. Cloudflare sends a one-time PIN on login. - Emails ending in — e.g.
@your-company.comfor whole-team access. - Identity provider — Google, GitHub, Okta, Azure AD, Microsoft 365, SAML, or generic OIDC. Configure these under Settings → Authentication before using them here.
- Country — geo-restrict (useful for admin panels).
- Device posture — require WARP client, a specific OS version, disk encryption, or the presence of a managed certificate. Posture is the piece that turns Access into real Zero Trust.
https://grafana.example.com. Instead of your Grafana login page you now see the Cloudflare Access authentication screen. After signing in, Cloudflare adds a CF-Access-Jwt-Assertion header and forwards the request through the tunnel to your origin. Your backend can trust this header because it is signed by Cloudflare and unspoofable — see developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json for verification details.The Access free tier includes up to 50 users, which is enough for most homelabs and small teams.
Step 9: WebSocket and Long-Lived Connections
Out of the box, cloudflared supports WebSockets and long-lived HTTP connections — you do not need to toggle anything on the free plan. But there are two things to know:
- Per-rule
noTLSVerifyand timeouts — If you have a service behind the tunnel that terminates its own TLS with a self-signed cert, addoriginRequest.noTLSVerify: trueto that ingress rule. For long-poll endpoints, bumpconnectTimeoutandtlsTimeout. - 100-second edge idle timeout — Cloudflare's edge closes idle connections after 100 seconds. For WebSockets this usually does not matter because the socket sends heartbeat frames. For raw TCP services that go quiet (some RDP workloads), you may see unexpected disconnects. Configure your app to send periodic keep-alives.
ingress:
- hostname: app.example.com
service: http://localhost:8080
originRequest:
connectTimeout: 30s
tlsTimeout: 30s
keepAliveTimeout: 90s
noHappyEyeballs: false
- service: http_status:404Full reference for originRequest options is at developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/cloudflared-parameters/origin-parameters.
Back Up Your Tunnel Configuration
Three files together are the "state" of your tunnel. Losing them means you cannot manage or reconnect it without recreating from scratch:
/root/.cloudflared/cert.pem— zone management cert from Step 3/root/.cloudflared/<uuid>.json— tunnel credentials from Step 4/etc/cloudflared/config.yml— ingress rules from Step 5
sudo tar czf cloudflared-backup-$(date +%Y%m%d).tar.gz \
/root/.cloudflared/cert.pem \
/root/.cloudflared/*.json \
/etc/cloudflared/config.yml
sudo chmod 600 cloudflared-backup-*.tar.gzStore the archive somewhere other than the VPS itself. When you restore on a new server, drop these three files back in place and cloudflared service install will pick them up.
Cautions: Bandwidth, TOS, and Streaming Content
Cloudflare Tunnel is free, but the free plan comes with one important restriction: Cloudflare's Terms of Service §2.8 prohibits using a Free, Pro, or Business plan to serve a disproportionate amount of non-HTML content — the classic example being a large video library or file-download service with heavy egress. Cloudflare occasionally enforces this by asking operators to move to an Enterprise plan or Cloudflare Stream.
Rules of thumb:
- Self-hosted dashboards, admin panels, APIs, blogs, SaaS apps — absolutely fine.
- A personal Jellyfin / Plex instance with a handful of users — typically fine but you are in a grey zone.
- Public video hosting, file sharing, or large media distribution — do not do this over a Free-plan tunnel. Use Cloudflare Stream, R2, or a proper CDN plan.
- Tunneling Bittorrent or other P2P traffic — explicitly forbidden.
cloudflared will happily push terabytes — but a sudden email from Cloudflare asking you to stop is not a fun surprise. Size your deployment against the TOS, not against what the tunnel technically allows.Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Tunnel keeps disconnecting / reconnecting | Firewall dropping outbound 7844 (QUIC) or unstable network | Force the HTTP/2 protocol: add protocol: http2 at the top of config.yml and restart. HTTP/2 goes over 443 which is almost always open. |
An A, AAAA, or CNAME record with that host already exists when running tunnel route dns | Leftover DNS record from a previous setup | Delete the conflicting record in the Cloudflare dashboard DNS tab, then rerun the command. |
| 502 Bad Gateway in the browser | Origin service not listening on the address in config.yml, or bound to an IP cloudflared cannot reach | Verify with curl http://localhost:8080 on the VPS. Check that the service binds to 127.0.0.1 or 0.0.0.0, not only a container-internal address. |
failed to request quick Tunnel: Post "https://api.trycloudflare.com/tunnel": ... | Running cloudflared tunnel --url (quick tunnel) instead of a named tunnel | Use the named-tunnel workflow from Step 4 onwards. Quick tunnels are for one-off testing only and cannot use Access. |
Cannot determine default origin certificate path | cert.pem missing or unreadable by the cloudflared user | Re-run cloudflared tunnel login, or copy cert.pem to /etc/cloudflared/ and chown cloudflared:cloudflared. |
| Access login page does not appear, app loads unprotected | Access Application domain does not exactly match the tunneled hostname | In the Access dashboard, confirm the Application domain matches the hostname in your ingress rule character-for-character (including subdomain). |
SSH through ssh.example.com hangs | Client not using cloudflared access ssh | On the client side, install cloudflared and use cloudflared access ssh --hostname ssh.example.com, or configure ProxyCommand in ~/.ssh/config. |
cloudflared using too much CPU | QUIC fallback loop on unstable network | Pin the protocol to http2 and check for packet loss to Cloudflare edge with mtr. |
sudo journalctl -u cloudflared -fGet the last 100 lines:
sudo journalctl -u cloudflared -n 100 --no-pagerIncrease log verbosity by editing config.yml:
loglevel: debug
transport-loglevel: infoThen sudo systemctl restart cloudflared.
FAQ
Does Cloudflare Tunnel expose my VPS IP address?
No. All traffic reaches your server through the outbound tunnel, so the origin IP never appears in DNS or in response headers. This is a big upgrade over running Cloudflare as a standard reverse proxy (orange cloud on A/AAAA records), where a misconfigured app or a historical DNS lookup can leak the origin IP. With Tunnel, there is no A record pointing at your VPS to leak.
Can I run multiple tunnels on the same VPS?
Yes, but it is rarely necessary. One tunnel can serve unlimited hostnames through ingress rules. Multiple tunnels on one box are useful if you want to isolate blast radius (a credential leak on one tunnel does not compromise the others) or run separate tunnels for separate Cloudflare accounts. Each instance needs its own config.yml and its own systemd unit — run them as cloudflared@<name>.service style instanced units.
How do I update cloudflared?
Because you installed from the apt repo, updates come through sudo apt update && sudo apt upgrade. Restart the service after an upgrade to pick up the new binary: sudo systemctl restart cloudflared. You can also enable Cloudflare's built-in auto-update by removing --no-autoupdate from the generated systemd unit, but most production operators prefer to control the update window themselves.
Can I use Cloudflare Tunnel for non-HTTP services like PostgreSQL or Redis?
Yes, via tcp:// ingress rules on the origin side and the cloudflared access tcp command (plus a WARP client or Access-for-Infrastructure policy) on the client side. Keep in mind that the free plan's terms are written around HTTP workloads — bulk non-HTTP traffic may trigger the same §2.8 conversation as streaming video. For legitimate small-scale admin access (psql from a laptop to your prod DB behind Access), it works beautifully.
Is there a Docker version of cloudflared?
Yes — cloudflare/cloudflared:latest on Docker Hub. For a single service on a single VPS, the apt install is simpler and integrates with systemd cleanly. Use the Docker image when you want to ship cloudflared alongside an app in a Compose stack or Kubernetes pod. The same config.yml and credentials file format applies; mount them into the container at /etc/cloudflared/.
How does Cloudflare Tunnel compare to Tailscale Funnel or ngrok?
Tailscale Funnel is built on Tailscale's WireGuard mesh and is excellent for devices that are already in your tailnet. Funnel is simpler than Cloudflare Tunnel but has a more limited feature set (fewer custom-domain options on the free plan, no Zero Trust policies, no WAF). ngrok is optimized for developer workflows — quick tunnels for local dev, webhooks, mobile testing — and its persistent-tunnel pricing gets expensive compared to free Cloudflare Tunnel. Cloudflare Tunnel wins on custom domains, integrated WAF, built-in DDoS protection, and Access policies. Pick Tunnel when you are already on Cloudflare DNS and need production hosting; pick Tailscale when you want a mesh VPN first and public exposure as a bonus; pick ngrok for ephemeral dev workflows.
Next Steps
Now that your tunnel is live, here are sensible follow-ups:
- Expose your apps cleanly through Nginx behind the tunnel — If you are hosting several apps on the same host, front them with a single Nginx reverse proxy and point the tunnel at Nginx instead of each service individually. See our Nginx install & reverse-proxy guide on Ubuntu 24.04 for the full Nginx setup.
- Put Docker services behind the tunnel — Ship
cloudflaredin the same Compose file as your app so new deployments bring up an authenticated public URL automatically. Our Docker install guide for Ubuntu 24.04 walks through Compose and networking basics. - Harden SSH — Close inbound 22 entirely and access the VPS with
cloudflared access sshplus an Access Application requiring your identity provider. This is one of the highest-leverage security upgrades you can make on a public VPS. - Add a WAF ruleset — Cloudflare's free Managed Ruleset blocks common OWASP Top 10 attacks before traffic ever reaches your origin. Enable it per-zone under Security → WAF.
- Monitor tunnel health — Cloudflare exposes tunnel metrics at
http://localhost:20241/metricsin Prometheus format when you setmetrics: localhost:20241inconfig.yml. Scrape it from Prometheus or point an Uptime Kuma probe at your public hostname.
Don't want to wrestle with DNS, certificates, and systemd?>
The CloudCore Starter plan from vps-server.host gives you an Ubuntu 24.04 VPS that is ready for Cloudflare Tunnel out of the box:>
- 2 vCPU, 4 GB RAM, 50 GB NVMe SSD
- Unmetered bandwidth at 1 Gbps
- Root access and full firewall control
- 24/7 monitoring and snapshot backups>
Launch your CloudCore Starter VPS and have your first tunnel live in the next 25 minutes.