How to Expose localhost to the Internet with Free HTTPS (Cloudflare Tunnel, No Port Forwarding)
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. Clo
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:
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.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.