Before you start: versions, package names and storage identifiers change over time. Check the commands against the official documentation for your setup before running them, especially anything that creates, deletes or overwrites data.
Introduction
By following this guide you will have a publicly reachable URL that forwards traffic to a service running on your home server without opening any inbound ports on your router. Cloudflare Access will protect that URL so only authorized identities can reach sensitive admin panels such as Grafana, Portainer or Home Assistant. The cloudflared tunnel will run as a persistent systemd service, automatically reconnecting if your ISP changes your public IP. You will also learn how to verify the setup, troubleshoot the most common issues, and optionally add HTTP‑to‑HTTPS redirects or expose additional services via sub‑domains. Expect to spend roughly 30‑45 minutes on the process, depending on how familiar you are with Linux and the Cloudflare dashboard.
More Tutorials & Self-Hosting guides on TechPulseMind →
Prerequisites
| Category | Requirement | Notes / Verification |
|---|---|---|
| Hardware | An always‑on home server (x86_64 or ARM) capable of running the service you want to expose | Minimum: 1 CPU core, 512 MiB RAM, network interface |
| Operating System | A Linux distribution that Cloudflare officially supports for cloudflared (e.g., Ubuntu 22.04 LTS, Debian 11, CentOS Stream 9, Raspberry Pi OS) | Run cat /etc/os-release to see your version; compare with the list in the Cloudflare Tunnel documentation |
| Software | – cloudflared binary (the Tunnel client) – Docker or a native service manager (systemd) if you prefer to run the tunnel as a service – The self‑hosted application you wish to expose (already running locally on a port, e.g., http://127.0.0.1:3000) |
Check the latest cloudflared release on the Cloudflare Developers site for installation instructions |
| Cloudflare Account | A Cloudflare account with access to the DNS zone for the domain you will use | You must be able to add CNAME records and create Access policies |
| Domain | A domain (or subdomain) already pointed to Cloudflare’s nameservers | Example: example.com managed in Cloudflare DNS |
| Network Access | Outbound TCP/443 (HTTPS) from the home server to the internet (no inbound ports needed) | Verify that your ISP does not block outbound 443 |
| Admin Rights | sudo/root on the home server to install packages, create systemd units, and modify files if needed | |
| Optional – Identity Provider | An IdP (Google Workspace, Azure AD, Okta, GitHub, etc.) if you want to use Cloudflare Access with SSO | Not required for simple PIN‑based policies |
Step‑by‑step procedure
-
First, install the cloudflared binary. Cloudflare provides a tarball for each architecture that always points to the latest release, so you do not need to hard‑code a version number. Determine your machine’s architecture with
uname -m; the output will be something likex86_64oraarch64. Use that to build the download URL.ARCH=$(uname -m) if [ "$ARCH" = "x86_64" ]; then URL="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.tgz" elif [ "$ARCH" = "aarch64" ]; then URL="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.tgz" else echo "Unsupported architecture: $ARCH" exit 1 fi curl -L "$URL" -o cloudflared.tgz tar -xzf cloudflared.tgz sudo cp cloudflared /usr/local/bin/ sudo chmod +x /usr/local/bin/cloudflared rm cloudflared.tgz cloudflaredThis sequence detects the architecture, downloads the appropriate tarball, extracts it, copies the binary to
/usr/local/binand makes it executable. The temporary files are cleaned up at the end. -
Next, authenticate cloudflared with your Cloudflare account. This step creates a certificate file that allows the client to act on your behalf. Running the command will open a browser window; you must log in to Cloudflare and select the account that owns the DNS zone.
sudo cloudflared tunnel loginAfter a successful login you will see a message indicating that a certificate was saved to
~/.cloudflared/cert.pem. Keep this file safe; it is required for all subsequent tunnel operations. -
Create a named tunnel. The name is purely for your convenience; the underlying identifier is a UUID that Cloudflare generates. Run the command below, replacing
with a descriptive label such ashome‑server.sudo cloudflared tunnel createThe output will display a UUID (the Tunnel ID) and confirm that a credentials file has been written to
~/.cloudflared/. Make a note of the UUID; you will need it in the configuration file..json -
Now create a configuration file that tells cloudflared how to forward traffic to your local service. The file uses YAML syntax; indentation matters. Open your preferred editor and create
~/.cloudflared/config.ymlwith the following contents, substituting the placeholders with your own values.tunnel:credentials-file: /home/ /.cloudflared/ .json ingress: - hostname: . service: http://localhost: - service: http_status:404 Explanation: the
tunnelline ties the config to the UUID you saved earlier. Thecredentials-filepoints to the JSON file created in step 3. Theingresslocalhost:. The final rule returns a 404 for any hostname that does not match a specific rule, preventing accidental exposure of other services. -
Test the tunnel manually before installing it as a service. This helps you catch configuration errors early. Run the tunnel in the foreground; you should see log lines indicating a successful connection to Cloudflare’s edge.
sudo cloudflared tunnel runLeave this command running in a separate terminal or background session while you proceed to the next step. If you see errors, double‑check the UUID, the credentials file path, and the
ingress -
Bind the tunnel to a DNS record. This creates a CNAME record that points your chosen subdomain to a Cloudflare‑generated address (
*.cfargotunnel.com). Traffic to that hostname will be proxied through the tunnel.sudo cloudflared tunnel route dns. After the command finishes, log in to the Cloudflare dashboard, navigate to DNS for your zone, and verify that a CNAME record exists for
pointing to something like. ..cfargotunnel.com -
Confirm that the service is reachable over HTTPS. Cloudflare automatically provisions a certificate for the hostname at its edge, so you do not need to manage TLS on the home server. Open a browser and visit
https://. You should see your application load with a padlock icon indicating a valid Cloudflare certificate.. -
Install cloudflared as a persistent systemd service so the tunnel starts automatically on boot and restarts if it fails. First, copy the sample service unit file to the systemd directory.
sudo cp /usr/local/bin/cloudflared /etc/systemd/system/cloudflared.serviceNow edit the file to contain the correct execution line. Replace
with the name you chose in step 3.[Unit] Description=Cloudflare Tunnel After=network-online.target Wants=network-online.target [Service] Type=simple User=root ExecStart=/usr/local/bin/cloudflared tunnel runRestart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target Explanation: the unit waits for the network to be online, runs cloudflared as root (so it can read the credentials file in
/root/.cloudflaredif you prefer, or adjust theUserline to match the account that owns the credentials), and restarts the process on failure after a five‑second delay. -
Reload the systemd manager configuration, enable the service to start at boot, and start it immediately.
sudo systemctl daemon-reload sudo systemctl enable --now cloudflared.service -
Verify that the service is active and healthy.
sudo systemctl status cloudflared.serviceYou should see
active (running). For a live view of logs, run:sudo journalctl -u cloudflared.service -fLook for lines that say “Connection established” and “healthy”. If you see repeated restarts, check the journal for error messages and revisit the configuration file.
-
Set up Cloudflare Access to protect the exposed hostname. In the Zero Trust dashboard:
- Navigate to **Access → Applications**.
- Click **Add an application**, choose **Self‑hosted**, and enter the same hostname you used in step 6 (
).. - Under **Access Policies**, create a new policy:
- **Action:** Allow
- **Include:** Select your Identity Provider (e.g., Google Workspace) or list specific email addresses.
- Optional: add a requirement for a security key, IP range, or device posture.
- Create a second policy (or change the default) to **Block** everyone else.
- Save the application.
-
Test the Access enforcement. From a browser logged in with an authorized identity, visit
https://; you should reach the service. Then open an incognito window or log out of the IdP and try again; you should see a Cloudflare Access “Access denied” page.. -
(Optional) Redirect plain HTTP traffic to HTTPS. This ensures that anyone who accidentally types the HTTP version ends up on the secure page.
- In the Cloudflare dashboard go to **SSL/TLS → Edge Certificates** and enable **Automatic HTTPS Rewrites** (this will rewrite HTTP to HTTPS for all hostnames in the zone).
- Alternatively, create a Page Rule:
http://→ Forwarding URL →. /* https://$1with status 301.
Verification Checklist
- cloudflared installed – Run
cloudflared --version; a version number should appear without error. - Authentication succeeded – Confirm that
~/.cloudflared/cert.pemexists and is readable. - Tunnel created – Execute
cloudflared tunnel list; yourshould appear with a UUID and status “healthy”. - Config file valid – Run
cloudflared tunnel --config ~/.cloudflared/config.yml validate; no errors should be reported. - Tunnel connects – Running
cloudflared tunnel runshould produce a log line similar to “Connection established”. - DNS CNAME created – In the Cloudflare DNS app, verify a CNAME record for
pointing to. ..cfargotunnel.com - Service reachable over HTTPS – Opening
https://in a browser loads your application with a valid Cloudflare certificate.. - Service runs as systemd –
systemctl is-active cloudflared.servicereturnsactive. - Persists after reboot – After
sudo reboot, repeat the previous check; the service should be active again. - Access policy works – Authorized login reaches the app; unauthorized login sees the Cloudflare Access denial page.
- HTTP→HTTPS redirect (if used) –
curl -I http://returns a 301 response with a. Location:header pointing to the HTTPS URL.
Common Failure Modes & Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
cloudflared tunnel run fails with “Error registering tunnel: tunnel already exists” |
A tunnel with the same name already exists under the account | List existing tunnels with cloudflared tunnel list. Either reuse the existing tunnel or delete it with cloudflared tunnel delete and recreate. |
No DNS record appears after tunnel route dns |
The command ran without sufficient API token permissions (missing DNS:Edit) | Generate an API Token with DNS:Edit zone permission, then repeat cloudflared tunnel login (or set CF_API_TOKEN environment variable) and run the route command again. |
| Service times out / 502 Bad Gateway | The tunnel cannot reach the local service (wrong address or port) | Verify the application is listening on localhost: using ss -ltnp | grep . Ensure no local firewall blocks localhost traffic (unlikely but check ufw or iptables). |
| After reboot, tunnel does not start | systemd service file references wrong user or path, or uses a relative path | Use the absolute path /usr/local/bin/cloudflared in ExecStart. Ensure the User line matches an account that can read the credentials file (root or the owning user). Reload with sudo systemctl daemon-reload. |
| Access policy lets everyone in | A more permissive “Allow *” rule is placed before the intended restrictive rule | In Zero Trust → Access → Applications → Policies, drag the restrictive rule to the top, or change the default action to “Block”. |
| Browser shows “Error 1006: Access denied” after login | The IdP returned an email not matching any rule, or the rule’s “Include” list is empty | Double‑check the IdP group/email list in the Access policy; ensure the logged‑in user’s email matches an allowed entry. |
| Tunnel disconnects frequently (flapping) | Unstable outbound ISP connection or Cloudflare rate‑limit due to many short‑lived connections | Confirm outbound TCP/443 is not throttled. Consider running the tunnel inside a Docker container with --restart unless-stopped to let it recover automatically. |
| Cannot reach the service locally after enabling tunnel | The ingress
|
Next steps / Optional enhancements
- Expose multiple services – Add additional
hostname:entries underingressinconfig.yml, each pointing to a different local port (e.g.,home‑assistant:8123,portainer:9000). Create matching DNS CNAME records withcloudflared tunnel route dns.sub. - Mutual TLS (mTLS) for extra hardening – Upload a client certificate to Cloudflare Access → Authentication → Client Certificates and require it in the Access policy. Configure cloudflared to present the certificate via
--origincertand--originkeyif your origin service validates client certs. - Log forwarding – Enable cloudflared’s built‑in logging to a file (
--logfile /var/log/cloudflared.log) or to syslog, then forward the logs to your SIEM or Loki instance for long‑term retention. - Monitor tunnel health – Start cloudflared with the
--metrics localhost:2000flag to expose Prometheus metrics, then scrape them with Prometheus and visualize in Grafana. - Backup credentials – Securely copy
~/.cloudflared/*.jsonandcert.pemto an encrypted off‑site backup (e.g., using Borg + rclone to a B2 bucket). Losing these files forces you to recreate the tunnel and update DNS.
When you have completed the steps above, you will have a self‑hosted service reachable via a trusted Cloudflare URL, protected by granular Access policies, and running reliably as a background service. From here you can continue to expand your homelab with additional applications, experiment with more advanced Zero Trust rules, or integrate monitoring and alerting to keep everything running smoothly.
Some links on this page may be affiliate links. If you buy through them we may earn a commission at no extra cost to you. See our affiliate disclosure.