Most home and mobile ISPs put you behind NAT or CGNAT. Your laptop only has a private address like 192.168.x.x, so nobody on the internet can reach your local server, and router port forwarding usually doesn't help.
Cloudflare Tunnel solves this the other way around. Instead of the internet connecting in to your laptop, a small program called cloudflared on your laptop connects out to Cloudflare. Cloudflare then forwards visitors from your public domain through that connection to your local app.
What you get:
- A permanent URL like
https://app.yourdomain.com - Free HTTPS, with no certificate setup
- No open ports and no public IP needed
How it works
Visitor → app.yourdomain.com → Cloudflare → tunnel → cloudflared (your laptop) → localhost:3000 What you need
- A free Cloudflare account
- A domain added to Cloudflare with status Active
- An app running locally, for example on
http://localhost:3000
Step 0: Add your domain to Cloudflare
Cloudflare can only create a tunnel for a domain it manages DNS for. If you bought your domain elsewhere:
- In the Cloudflare dashboard, click Add a domain and choose the free plan.
- Cloudflare shows you two nameservers.
- At your registrar (where you bought the domain), replace the existing nameservers with those two.
- Wait until the domain shows Active. This can take a few minutes to a few hours.
Step 1: Install cloudflared
cloudflared is the program that runs on your laptop and keeps the outbound connection to Cloudflare alive.
Ubuntu / Debian:
sudo mkdir -p --mode=0755 /usr/share/keyrings Creates the folder where trusted package signing keys are stored.
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null Downloads Cloudflare's signing key so your system can verify the package really comes from Cloudflare.
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \ https://pkg.cloudflare.com/cloudflared any main" \ | sudo tee /etc/apt/sources.list.d/cloudflared.list Adds Cloudflare's package repository to apt, so apt knows where to download cloudflared from.
sudo apt update && sudo apt install cloudflared Refreshes the package list (so it sees the new repository) and installs cloudflared.
cloudflared --version Confirms the install worked and prints the version.
On macOS use brew install cloudflared. On Windows use:
winget install --id Cloudflare.cloudflared Step 2: Log in to Cloudflare
cloudflared tunnel login Why: This links your laptop to your Cloudflare account and chooses which domain the tunnel may use. A browser page opens. Select your domain and click Authorize.
When it succeeds, it saves a certificate file called cert.pem in ~/.cloudflared/. The next command needs this file to prove you're allowed to create tunnels.
Don't press
Ctrl+Cwhile the terminal is waiting. If you cancel,cert.pemis never saved and the next step fails withCannot determine default origin certificate path.
Check that it exists:
ls ~/.cloudflared/cert.pem Why: Confirms the login finished before you continue.
Step 3: Create the tunnel
cloudflared tunnel create my-laptop Why: This registers a new tunnel named my-laptop with Cloudflare. It prints a tunnel ID (a long UUID) and saves a credentials file at ~/.cloudflared/<TUNNEL_ID>.json.
That JSON file is the tunnel's secret key. It's what lets your laptop prove it owns this tunnel, so keep it private and never commit it to Git.
Step 4: Point a hostname at the tunnel
cloudflared tunnel route dns my-laptop \ app.yourdomain.com Why: A tunnel by itself has no public address. This command creates a DNS record in Cloudflare so that app.yourdomain.com points to your tunnel. Replace app.yourdomain.com with the real hostname you want, such as api.example.com.
Step 5: Create the config file
Create ~/.cloudflared/config.yml:
tunnel: <TUNNEL_ID> credentials-file: /home/YOURUSER/.cloudflared/<TUNNEL_ID>.json ingress: - hostname: app.yourdomain.com service: http://localhost:3000 - service: http_status:404 Why each line matters:
-
tunneltellscloudflaredwhich tunnel to run (use the ID from Step 3). -
credentials-fileis the path to the secret JSON from Step 3. -
ingressis the routing table. It says "requests for this hostname go to this local service." -
hostname: app.yourdomain.commatches the DNS record from Step 4. -
service: http://localhost:3000is your local app. Change the port to match yours. - The last rule,
http_status:404, is a required catch-all that answers any request that matches no hostname.
Step 6: Run the tunnel
cloudflared tunnel run my-laptop Why: This starts the tunnel. cloudflared opens an outbound connection to Cloudflare and begins forwarding traffic to your local app.
Now open https://app.yourdomain.com. You should see your local app, served over HTTPS automatically. Cloudflare handles the certificate for you.
If you stop this command (Ctrl+C), the site goes offline. The next step fixes that.
Step 7: Run it automatically on boot
So you don't have to keep a terminal open, install it as a system service.
sudo mkdir -p /etc/cloudflared Why: Creates the system-wide config folder that the service reads from.
sudo cp ~/.cloudflared/config.yml \ /etc/cloudflared/config.yml sudo cp ~/.cloudflared/<TUNNEL_ID>.json \ /etc/cloudflared/ Why: The service runs as root, not as your user, so it can't read files in your home folder. These commands copy the config and credentials to where it can.
Then edit /etc/cloudflared/config.yml and change the credentials path to:
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json Why: The file moved, so the path must match its new location.
sudo cloudflared service install Why: Registers cloudflared as a systemd service.
sudo systemctl enable --now cloudflared Why: enable makes it start at every boot, and --now starts it immediately.
systemctl status cloudflared Why: Shows whether the service is running. You want to see active (running).
Optional: expose more than one app
Add more entries under ingress:
ingress: - hostname: app.yourdomain.com service: http://localhost:3000 - hostname: api.yourdomain.com service: http://localhost:8000 - hostname: ssh.yourdomain.com service: ssh://localhost:22 - service: http_status:404 For each new hostname, run the DNS command from Step 4:
cloudflared tunnel route dns my-laptop \ api.yourdomain.com Why: Each hostname needs its own DNS record pointing at the tunnel. Restart the service afterwards:
sudo systemctl restart cloudflared SSH through the tunnel
On the machine you're connecting from, install cloudflared and add this to ~/.ssh/config:
Host ssh.yourdomain.com ProxyCommand cloudflared access ssh \ --hostname %h Why: SSH isn't a web protocol, so it needs cloudflared on the client side to wrap the connection. Then connect normally:
ssh youruser@ssh.yourdomain.com Note: in
~/.ssh/config, a backslash does not continue a line. If your SSH version rejects the splitProxyCommandabove, put it back on one line:ProxyCommand cloudflared access ssh --hostname %h.
Prefer a UI? Dashboard method
If you don't want to edit YAML:
- Open the Cloudflare Zero Trust dashboard.
- Go to Networks → Tunnels → Create a tunnel.
- Copy the install command it shows and run it on your laptop. This installs the tunnel as a service using a token.
- In the Public Hostname tab, add
app.yourdomain.comand set the service tohttp://localhost:3000.
Menu names change sometimes, but the flow stays the same.
Troubleshooting
Cannot determine default origin certificate path or Error locating origin cert
There's no cert.pem. The login in Step 2 didn't finish, usually because of Ctrl+C, or no domain was authorized. Run cloudflared tunnel login again, select your domain, click Authorize, and wait for the success message.
The authorize page shows no domains
Your domain isn't added to Cloudflare yet, it's still Pending, or you're logged into a different Cloudflare account.
502 Bad Gateway or error 1033
Your local app isn't running, the port in service: is wrong, or the tunnel isn't running. Check systemctl status cloudflared.
The site doesn't load but the tunnel is running
Check the YAML indentation, make sure the catch-all rule is last, and make sure the hostname in config.yml matches the DNS record exactly.
Useful commands
cloudflared tunnel list Lists all your tunnels.
cloudflared tunnel info my-laptop Shows whether the tunnel has active connections.
cloudflared tunnel ingress validate Checks your config.yml for mistakes.
journalctl -u cloudflared -f Streams live logs from the service, which is the best place to see why something fails.
Security tips
- Don't expose SSH or admin panels openly. Put them behind Cloudflare Access so a login is required first.
- Keep
cert.pem, the tunnel credentials JSON and any tokens out of Git and out of screenshots. - Add authentication to your own API. A public URL means anyone on the internet can reach it.
- Disable sleep on the laptop and run your app as a service so it restarts after a reboot.
- Cloudflare's terms are aimed at web traffic, so avoid pushing large video or file transfers through the tunnel.
Conclusion
With your own domain, Cloudflare Tunnel gives you a permanent HTTPS URL for a laptop server without opening a single port or needing a public IP. The key ideas are: log in once, create a tunnel, point a hostname at it, describe the routing in config.yml, and run it as a service.
What are you hosting from your laptop? Let me know in the comments.
