DEV Community

Safal Bhattarai
Safal Bhattarai

Posted on

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.

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. In the Cloudflare dashboard, click Add a domain and choose the free plan.
  2. Cloudflare shows you two nameservers.
  3. At your registrar (where you bought the domain), replace the existing nameservers with those two.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Adds Cloudflare's package repository to apt, so apt knows where to download cloudflared from.

sudo apt update && sudo apt install cloudflared
Enter fullscreen mode Exit fullscreen mode

Refreshes the package list (so it sees the new repository) and installs cloudflared.

cloudflared --version
Enter fullscreen mode Exit fullscreen mode

Confirms the install worked and prints the version.

On macOS use brew install cloudflared. On Windows use:

winget install --id Cloudflare.cloudflared
Enter fullscreen mode Exit fullscreen mode

Step 2: Log in to Cloudflare

cloudflared tunnel login
Enter fullscreen mode Exit fullscreen mode

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+C while the terminal is waiting. If you cancel, cert.pem is never saved and the next step fails with Cannot determine default origin certificate path.

Check that it exists:

ls ~/.cloudflared/cert.pem
Enter fullscreen mode Exit fullscreen mode

Why: Confirms the login finished before you continue.

Step 3: Create the tunnel

cloudflared tunnel create my-laptop
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Why each line matters:

  • tunnel tells cloudflared which tunnel to run (use the ID from Step 3).
  • credentials-file is the path to the secret JSON from Step 3.
  • ingress is the routing table. It says "requests for this hostname go to this local service."
  • hostname: app.yourdomain.com matches the DNS record from Step 4.
  • service: http://localhost:3000 is 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Why: The file moved, so the path must match its new location.

sudo cloudflared service install
Enter fullscreen mode Exit fullscreen mode

Why: Registers cloudflared as a systemd service.

sudo systemctl enable --now cloudflared
Enter fullscreen mode Exit fullscreen mode

Why: enable makes it start at every boot, and --now starts it immediately.

systemctl status cloudflared
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For each new hostname, run the DNS command from Step 4:

cloudflared tunnel route dns my-laptop \
  api.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Why: Each hostname needs its own DNS record pointing at the tunnel. Restart the service afterwards:

sudo systemctl restart cloudflared
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Note: in ~/.ssh/config, a backslash does not continue a line. If your SSH version rejects the split ProxyCommand above, 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:

  1. Open the Cloudflare Zero Trust dashboard.
  2. Go to Networks → Tunnels → Create a tunnel.
  3. Copy the install command it shows and run it on your laptop. This installs the tunnel as a service using a token.
  4. In the Public Hostname tab, add app.yourdomain.com and set the service to http://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
Enter fullscreen mode Exit fullscreen mode

Lists all your tunnels.

cloudflared tunnel info my-laptop
Enter fullscreen mode Exit fullscreen mode

Shows whether the tunnel has active connections.

cloudflared tunnel ingress validate
Enter fullscreen mode Exit fullscreen mode

Checks your config.yml for mistakes.

journalctl -u cloudflared -f
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)