How to Expose Your Home Server with Cloudflare Tunnel

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

  1. 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 like x86_64 or aarch64. 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 cloudflared
    

    This sequence detects the architecture, downloads the appropriate tarball, extracts it, copies the binary to /usr/local/bin and makes it executable. The temporary files are cleaned up at the end.

  2. 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 login
    

    After 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.

  3. 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 as home‑server.

    sudo cloudflared tunnel create 
    

    The output will display a UUID (the Tunnel ID) and confirm that a credentials file has been written to ~/.cloudflared/.json. Make a note of the UUID; you will need it in the configuration file.

  4. 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.yml with 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 tunnel line ties the config to the UUID you saved earlier. The credentials-file points to the JSON file created in step 3. The ingresslocalhost:. The final rule returns a 404 for any hostname that does not match a specific rule, preventing accidental exposure of other services.

  5. 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 run 
    

    Leave 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

  6. 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.

  7. 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.

  8. 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.service
    

    Now 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 run 
    Restart=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/.cloudflared if you prefer, or adjust the User line to match the account that owns the credentials), and restarts the process on failure after a five‑second delay.

  9. 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
    
  10. Verify that the service is active and healthy.

    sudo systemctl status cloudflared.service
    

    You should see active (running). For a live view of logs, run:

    sudo journalctl -u cloudflared.service -f
    

    Look for lines that say “Connection established” and “healthy”. If you see repeated restarts, check the journal for error messages and revisit the configuration file.

  11. Set up Cloudflare Access to protect the exposed hostname. In the Zero Trust dashboard:

    1. Navigate to **Access → Applications**.
    2. Click **Add an application**, choose **Self‑hosted**, and enter the same hostname you used in step 6 (.).
    3. 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.
    4. Create a second policy (or change the default) to **Block** everyone else.
    5. Save the application.
  12. 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.

  13. (Optional) Redirect plain HTTP traffic to HTTPS. This ensures that anyone who accidentally types the HTTP version ends up on the secure page.

    1. 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).
    2. Alternatively, create a Page Rule: http://./* → Forwarding URL → https://$1 with status 301.

Verification Checklist

  1. cloudflared installed – Run cloudflared --version; a version number should appear without error.
  2. Authentication succeeded – Confirm that ~/.cloudflared/cert.pem exists and is readable.
  3. Tunnel created – Execute cloudflared tunnel list; your should appear with a UUID and status “healthy”.
  4. Config file valid – Run cloudflared tunnel --config ~/.cloudflared/config.yml validate; no errors should be reported.
  5. Tunnel connects – Running cloudflared tunnel run should produce a log line similar to “Connection established”.
  6. DNS CNAME created – In the Cloudflare DNS app, verify a CNAME record for . pointing to .cfargotunnel.com.
  7. Service reachable over HTTPS – Opening https://. in a browser loads your application with a valid Cloudflare certificate.
  8. Service runs as systemdsystemctl is-active cloudflared.service returns active.
  9. Persists after reboot – After sudo reboot, repeat the previous check; the service should be active again.
  10. Access policy works – Authorized login reaches the app; unauthorized login sees the Cloudflare Access denial page.
  11. 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 Make sure the ingressservice: http_status:404 (or a valid fallback) and that the specific hostname rule is placed above it.

Next steps / Optional enhancements

  • Expose multiple services – Add additional hostname: entries under ingress in config.yml, each pointing to a different local port (e.g., home‑assistant:8123, portainer:9000). Create matching DNS CNAME records with cloudflared 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 --origincert and --originkey if 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:2000 flag to expose Prometheus metrics, then scrape them with Prometheus and visualize in Grafana.
  • Backup credentials – Securely copy ~/.cloudflared/*.json and cert.pem to 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.

How to Set Up WordPress with Docker ComposeRead next