Watching My ACT Internet Watch Itself: Uptime Kuma + Cloudflare Tunnel

How I built a tiny status page that tells me — and anyone who asks — whether my home internet is behaving. No public IP. No port forwarding. No router gymnastics.

The problem

My ISP has opinions. Some evenings it delivers a crisp gigabit, other evenings it behaves like a damp sponge. And whenever I complain, the conversation goes like this:

“Sir, is the internet currently down?” “No, it’s up right now.” “Then there is no issue, sir.”

The outage is always yesterday. Support tickets need evidence, and the only evidence I had was my own increasingly frustrated memory. That’s a bad witness.

What I really wanted was a little dashboard sitting somewhere, quietly poking the internet every thirty seconds, drawing graphs I could screenshot and send. Bonus points if the rest of the household — and, honestly, the support agent — could just visit a URL and see the same thing I see.

Two sub-problems fell out of that:

  1. Something has to do the monitoring. It has to ping public hosts, log the results, and show me a timeline I can actually read.

  2. That something has to be reachable from outside my house. Without opening ports on my router, without a static public IP (I don’t have one), and without exposing my whole home network to the world.

The answers turned out to be Uptime Kuma for problem one, and Cloudflare Tunnel for problem two. Both are free, both are boringly reliable, and together they took a single evening to wire up.

Let’s walk through it.

uptime-kuma-ACT-internet-monitor-2

Getting it running

Assuming Docker is already installed, the entire install is a single docker run. That’s it. No compose file, no reverse proxy yet, nothing fancy:

docker run -d --restart=always -p 3001:3001 \
  -v uptime-kuma:/app/data \
  --name uptime-kuma \
  louislam/uptime-kuma:1

# sanity-check it came up, and tail the logs if you're curious
docker ps | grep uptime-kuma
docker logs uptime-kuma

A quick tour of the flags, because understanding them pays dividends later:

  • d runs it detached, in the background.
  • -restart=always means it comes back by itself after a reboot or crash. Set it once, forget it.
  • p 3001:3001 publishes the container’s port 3001 on the host.
  • v uptime-kuma:/app/data stores the SQLite database and all your monitor history in a named Docker volume. If you skip this flag, your data dies with the container. Don’t skip it.
  • louislam/uptime-kuma:1 pins to the v1 major, which gives you patch updates but no surprise breaking changes.

Open http://localhost:3001 in a browser, and the first screen will ask you to create an admin account. Pick a username, pick a password, done. You’re in.

Old laptop now working as a 24/7 Server. ACT internet monitoring runs on this system(Arch Linux)


Teaching it what to watch

Here’s the fun part. You’ve got a blank dashboard and a big green + Add New Monitor button. I wanted to answer one question: is my internet actually working right now, and if not, whose fault is it?

So I added four monitors, all running every 30 seconds:

  +---+------+----------------+------------------------+-----------------------------------------------------------------------+                             
  | # | Type | Name           | Target                 | Why                                                                   |                             
  +---+------+----------------+------------------------+-----------------------------------------------------------------------+                             
  | 1 | Ping | Google DNS     | 8.8.8.8                | Baseline: can I reach the internet at all?                            |                             
  | 2 | Ping | Cloudflare DNS | 1.1.1.1                | Second opinion on the same question, different provider.             |                              
  | 3 | HTTP | Google Website | https://google.com     | Catches DNS-resolution and TLS issues that a raw ping would miss.     |                             
  | 4 | HTTP | ISP Website    | https://<your-isp>.com | If this goes down but Google stays up, the problem is likely my ISP.  |                             
  +---+------+----------------+------------------------+-----------------------------------------------------------------------+  

That last one is the clever bit, and I can’t take credit for the idea — it’s the oldest trick in the sysadmin book. Always include a monitor that fails independently from the others. Two pings to two different DNS providers catch most outages, but when the ISP’s own infrastructure goes wobbly, their status page is often the first thing to stutter. It’s a tiny built-in lie-detector.

Thirty seconds is aggressive — most tutorials suggest 60 — but I wanted high-resolution graphs for exactly those nasty flap-flap-flap micro-outages that are impossible to catch by hand.

Give it a minute or two, refresh the dashboard, and the graphs start filling in. At this point you have a fully functional internet health monitor. Sitting on your laptop. Where nobody else can see it.

Which brings us to the second problem.


Part 2 — Letting the outside world in (without actually letting it in)

Here was my situation: a working dashboard on localhost:3001, and a desire to show it to people who were not me. The obvious options are all slightly awful:

  • Port-forward 3001 on the router. Requires a static public IP (I don’t have one), exposes my home network to drive-by scans, and I have to run my own TLS cert.

  • Rent a tiny VPS and reverse-proxy back home. Works, but now I’m paying for a VPS, managing a second machine, and building a WireGuard link just to show a graph.

  • Use an ngrokstyle free tunnel. Fine for demos, awful for “this should just stay up forever” — URLs change, free tiers rate-limit.

Then there’s the option I landed on: Cloudflare Tunnel, via the cloudflared daemon. It’s free for this kind of personal use, and the mental model is genuinely elegant once it clicks.

How Cloudflare Tunnel actually works

Most “expose my local service” tools try to punch a hole inbound into your network. Cloudflare Tunnel does the opposite: it makes a persistent outbound connection from your machine to Cloudflare’s edge network, and then keeps that connection open. When someone visits your public URL, Cloudflare already has a pipe straight to your machine — it just shoves the request down it.

The consequences are delightful:

  • No public IP needed. The tunnel is outbound, and outbound traffic just works on any home internet.

  • No router configuration. Nothing is opened, nothing is forwarded, your router has no idea any of this is happening.

  • No inbound firewall holes. There’s literally nothing for a drive-by scanner to find.

  • TLS is terminated at Cloudflare. Your visitors get a green padlock automatically, and you don’t touch a certificate file.

  • DDoS protection and caching come along for the ride, because Cloudflare is Cloudflare.

The one prerequisite is that you need a domain whose nameservers are pointed at Cloudflare. I’m using example.com as the placeholder throughout this post — substitute your own.

Installing cloudflared, logging in, and creating the tunnel

On Arch, cloudflared is in the extra repo, so no AUR dance. On other distros, grab it from Cloudflare’s docs — the rest of the commands are identical.

The first three things you do are: install it, log in (which is a browser flow that drops a certificate in your home directory), and create a named tunnel. I’ll batch them because they belong together:

           ┌──────────────┐                                                                                                                                 
            │   Visitor    │                                                                                                                                 
            │  (browser)   │                                                                                                                                 
            └──────┬───────┘                                                                                                                                 
                   │  1. https://status.example.com         
                   │     DNS lookup → CNAME → <tunnel-id>.cfargotunnel.com                                                                                   
                   ▼                                                                                                                                         
       ┌───────────────────────┐                                                                                                                             
       │   Cloudflare Edge     │  ◄── TLS terminated here                                                                                                    
       │  (Mumbai, Chennai,    │      DDoS protection, caching                                                                                               
       │   wherever you are)   │                                                                                                                             
       └───────────┬───────────┘                                                                                                                             
                   │  2. Request routed down the                                                                                                             
                   │     pre-established tunnel                                                                                                              
                   │     (matched by tunnel ID)
                   │                                                                                                                                         
                   │         ╔════════════════════╗         
                   │         ║   OUTBOUND-ONLY    ║                                                                                                          
                   │         ║  persistent HTTPS  ║         
                   │         ║   connection(s)    ║                                                                                                          
                   │         ╚════════════════════╝         
                   │                  ▲                                                                                                                      
                   │                  │ 0. Opened at boot by
                   ▼                  │    `cloudflared` daemon                                                                                              
       ┌───────────────────────┐     │                                                                                                                       
       │   Home Router / NAT   │     │   (no inbound ports,                                                                                                  
       │  (nothing forwarded)  │     │    no public IP needed)                                                                                               
       └───────────┬───────────┘     │                                                                                                                       
                   │                  │                                                                                                                      
                   ▼                  │                                                                                                                      
       ┌───────────────────────┐     │                      
       │    Your Machine       │     │                                                                                                                       
       │                       │     │                      
       │  ┌─────────────────┐  │     │                                                                                                                       
       │  │   cloudflared   │──┼─────┘
       │  │    (systemd)    │  │                                                                                                                             
       │  └────────┬────────┘  │                            
       │           │ 3. Proxied to                                                                                                                           
       │           │    http://localhost:3001               
       │           ▼           │                                                                                                                             
       │  ┌─────────────────┐  │                            
       │  │  Uptime Kuma    │  │                                                                                                                             
       │  │  (Docker, 3001) │  │                                                                                                                             
       │  └─────────────────┘  │                                                                                                                             
       └───────────────────────┘                        
# 1. Install and verify
sudo pacman -S cloudflared
cloudflared --version

# 2. Log in via the browser — a URL prints, you click it, you pick the zone
cloudflared tunnel login

# 3. Make sure the cert landed in the right place
mkdir -p ~/.cloudflared
mv ~/Downloads/cert.pem ~/.cloudflared/cert.pem 2>/dev/null || true

# 4. Create a named tunnel called "uptime-kuma"
cloudflared tunnel create uptime-kuma

A quick gloss on what just happened:

  • cloudflared tunnel login opens a browser page where you pick which Cloudflare zone (i.e., domain) this machine is allowed to manage. When you click Authorize, a cert.pem downloads. This is an account-level origin certificate — it lets this machine run tunnel/DNS management commands for that zone. It’s not used to serve live traffic.
  • cloudflared tunnel create uptime-kuma does two things: it creates a long-lived “named tunnel” object on Cloudflare’s side, and it writes a credentials JSON file to ~/.cloudflared/<tunnel-id>.json. That JSON file is the secret that authenticates this machine as the tunnel. Treat it like a private key — anyone with it can run the tunnel and intercept your traffic.

The create command prints the new tunnel ID. Copy it somewhere, you’ll need it once.

Routing DNS, writing the config, and a first test

Now we need to tell Cloudflare “when somebody visits status.example.com, route it down this tunnel,” and we need to tell cloudflared “when requests arrive for that hostname, forward them to localhost:3001.” That’s a DNS route plus an ingress config.

Do all of that — plus a manual test run — in one go:

# 1. Create the DNS CNAME (Cloudflare does this for you, no dashboard clicks)
cloudflared tunnel route dns uptime-kuma status.example.com

# 2. Write the config file
cat > ~/.cloudflared/config.yml <<'YAML'
tunnel: 
credentials-file: /home//.cloudflared/.json

ingress:
  - hostname: status.example.com
    service: http://localhost:3001
  - service: http_status:404
YAML

# 3. Run the tunnel in the foreground as a smoke test
cloudflared tunnel run uptime-kuma

Two things to understand about that config file:

  1. Ingress rules are evaluated top-to-bottom, like a routing table. The first rule says “anything for status.example.com goes to our local Uptime Kuma.” You can add more hostnames later — just stack them above the catch-all.
  2. That final http_status: 404 entry is required. cloudflared refuses to start without a catch-all. It exists to return a clean 404 for any hostname that doesn’t match the rules above it.

Once the tunnel is running in the foreground, open https://status.example.com in your browser. If everything is wired up correctly, you’ll see your Uptime Kuma dashboard — served over HTTPS, fronted by Cloudflare, from a laptop that has no public IP and no open ports. It feels a little like magic the first time. It should.

Hit Ctrl-C to stop it, because we don’t want to run it in a terminal forever.

Making it boot-proof with systemd

A tunnel you have to remember to start is a tunnel that will be down the next time you actually need it. Let’s hand it to systemd.

Create a unit file that runs cloudflared as your regular user (not root — least privilege, and the credentials already live in your home directory), then enable and verify it:

sudo tee /etc/systemd/system/cloudflared.service > /dev/null <<'UNIT'
[Unit]
Description=cloudflared tunnel (uptime-kuma)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=
Group=
ExecStart=/usr/bin/cloudflared --config /home//.cloudflared/config.yml tunnel run uptime-kuma
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager

A few small choices baked into that unit that are worth naming out loud:

  • User=<user> / Group=<user>. cloudflared does not need root. Running it as your normal user means a bug in cloudflared can’t rewrite /etc.
  • After=network-online.target / Wants=network-online.target. On boot, wait until the network is actually up before trying to dial Cloudflare. Otherwise you get a pile of angry restarts in the logs for the first 15 seconds of every boot.
  • Restart=on-failure with a 5-second delay. If the process dies unexpectedly, systemd will bring it right back.

If systemctl status shows active (running) and a log line that says something like Registered tunnel connection, you’re done. The tunnel will now come up on every boot, automatically, with no input from you.


Polishing the dashboard

One last touch in the Uptime Kuma UI, under Settings → General:

  • Primary Base URLhttps://status.example.com. This tells Uptime Kuma what its public address is, so generated links (email notifications, webhooks) point at the right place.
  • Entry Page → set it to your status page instead of the dashboard. This way, random visitors get a clean “is the internet up?” view, while you log in at /dashboard yourself.

That’s it. The whole system is now:

  • Uptime Kuma pinging the internet every 30 seconds from inside my house, storing history in a Docker volume.
  • cloudflared holding a persistent outbound connection to Cloudflare’s edge, supervised by systemd.
  • Cloudflare answering public DNS for status.example.com, terminating TLS, and forwarding requests down the tunnel to localhost:3001.
  • A public status page I can send to anyone, including the support agent who keeps asking “is it currently down.”

The commands I actually use day-to-day

Once it’s all running, you barely touch it. But these are the half-dozen commands worth memorising for when you do:

# Is the tunnel healthy? What does it think of the world?
sudo systemctl status cloudflared
journalctl -u cloudflared -f           # live log tail

# Changed config.yml? Reload the tunnel without dropping DNS
sudo systemctl restart cloudflared

# Is Uptime Kuma still up? (it almost always is, honestly)
docker ps | grep uptime-kuma
docker logs uptime-kuma
docker restart uptime-kuma

And two lookup commands from the cloudflared side that are occasionally handy:

cloudflared tunnel list                # all named tunnels on this account
cloudflared tunnel info uptime-kuma    # connection status per edge datacenter

When things go sideways

Three failure modes, in roughly the order I’ve hit them:

  • Cloudflare error 1033 (“Argo Tunnel error”). The cloudflared service is down. systemctl status cloudflared and journalctl -u cloudflared -n 50 will tell you why. Most commonly: a typo in config.yml after an edit.
  • 502 / 521 from Cloudflare. The tunnel is up, but cloudflared can’t reach localhost:3001. That means Uptime Kuma itself is down — check docker ps and docker logs uptime-kuma.
  • DNS doesn’t resolve at all. Check the Cloudflare dashboard → your zone → DNS. There should be a CNAME record status<tunnel-id>.cfargotunnel.com, proxied (orange cloud). If it’s missing, re-run cloudflared tunnel route dns uptime-kuma status.example.com.
# 1. Route the new hostname to the same tunnel
cloudflared tunnel route dns uptime-kuma speed.example.com

# 2. Edit ~/.cloudflared/config.yml and add an ingress entry ABOVE the 404
#      - hostname: speed.example.com
#        service: http://localhost:8080

# 3. Reload
sudo systemctl restart cloudflared

One tunnel, many services, zero extra ports. It’s a surprisingly tidy way to run a home lab.

Security notes I shouldn’t have to say but will anyway

  • The status page is public by design — that’s the point. Uptime Kuma’s admin UI is still behind a login, but don’t put anything sensitive on a status page you’d rather not have indexed by Google.
  • ~/.cloudflared/ should be 700 and owned by your user. The credentials JSON file in there is a secret. Don’t commit it, don’t share it, don’t paste it into a screenshot. If it leaks, delete the tunnel and recreate it.
  • If you ever want to put the dashboard behind an email-login gate (not just the admin account), Cloudflare Zero Trust → Access → Applications will do it for free for up to 50 users. That’s a whole other blog post.

What changed for me

Having the dashboard exposed has genuinely changed how I talk to my ISP. Instead of “it’s been bad lately, can you check,” I can say “here are the four micro-outages between 21:14 and 22:02 yesterday, here’s a public link, please investigate.” The graphs do the arguing for me.

More than that, though, I’m surprised by how much I enjoy the shape of this setup. Uptime Kuma doesn’t phone home. Cloudflare Tunnel doesn’t require a VPS. There’s no SaaS subscription, no account I have to remember to renew, no magic I can’t see. It’s a Docker container, a systemd unit, and a config file — three boring primitives doing one useful thing together. That’s my favourite kind of infrastructure.

If you’ve been putting off building your own little status page because the networking felt intimidating, I hope this was enough of a nudge. It is, genuinely, an evening’s work. Go poke your internet.

Previous
Previous

Pi-hole setup in Raspberry Pi 4

Next
Next

Two Week(14-days) Japan Itinerary from India