Platform

Monitor a Self-Hosted App Stack You Run Yourself

10 min read

Here is the structural problem: a monitor that lives on, or right next to, the thing it watches cannot, by construction, see that thing become unreachable from the public internet. When the box goes down it takes the monitor with it. When the home connection drops, both go quiet together. To know whether real users can actually reach your self-hosted app, you need a monitor that runs somewhere else, off your box and off your network. This post is about adding that one external layer on top of the internal tooling you already run, not ripping anything out. Start for free.

At around 00:40 local time on March 10, 2021, a fire broke out in a power-supply room at OVHcloud's Strasbourg campus. As a precaution, electricity was cut to the entire site, which took SBG1 through SBG4 offline at once, and SBG2, a building holding roughly 14,046 servers, was destroyed. OVH told customers in plain language to activate their disaster recovery plan (The Register, March 10 2021; see also the OVHcloud newsroom).

Now run the homelab version of that event in your head. Your app is on a box in the closet, or on a single VPS. Your monitor, the thing whose whole job is to tell you the app went down, is a container on the same box, or another VPS in the same datacenter. The power goes. The uplink goes. The site goes. And your monitor goes dark right alongside the thing it was supposed to be watching, so the alert you were counting on never fires. It is not that the monitor failed. It is that it could never have succeeded. A probe that shares fate with the app cannot report the app unreachable, because the same event that makes the app unreachable also silences the probe.

The blind spot is not Kuma. It is anything that shares fate with your box.

It is tempting to make this a story about one tool, but the problem is structural, not a product flaw. A self-hosted Uptime Kuma instance has the blind spot. So does a Prometheus blackbox exporter scraping your app from the same network, a Netdata agent on the host, a curl-cron in a tmux session, a self-hosted Healthchecks.io receiving dead-man pings, and the app's own /healthendpoint. Every one of them is excellent at what it does. Every one of them sits on or beside the box and depends on that box, its network, and its power to function. The moment the failure is "the whole thing is unreachable from outside," the on-box observer is part of what went dark.

This is not an argument against self-hosting your monitoring. It is an argument about where one specific question has to be answered from. If you want the tool-by-tool comparison of self-hosted Uptime Kuma against a hosted service, including the real cost of running it yourself, that is a separate post: Uptime Kuma vs hosted monitoring. Here, the only claim is narrower: the observer that has to confirm public reachability cannot be one that shares fate with the thing it watches.

The five things external monitoring sees that your box can't

Here are the failure shapes where a same-host probe shows green, or shows nothing at all, while a real user on the public internet gets nothing back.

1. Host down

A kernel panic, an out-of-memory kill that takes the box with it, a full disk that wedges every write, or a power loss. The host is gone, and so is any monitor running on it. An external probe simply stops getting answers and alerts.

2. Outbound or network partition

The box is up and the app is happily serving on localhost, but the uplink is down or a firewall rule has cut the path to the internet. An on-box check passes because the app answers locally. Nobody outside can reach it. Only a probe coming from outside notices the partition.

3. Regional DNS or CDN failure

Your app is fine, but the DNS record fails to resolve in some resolvers, or the CDN or proxy in front of it returns errors in one region. A single-location check sees whichever path happens to work. A multi-region external monitor catches the market where resolution or the edge is broken.

4. Whole-datacenter outage

The OVH-class event, or a Hetzner-class one. Power, cooling, or networking for the entire facility goes, and every box you have there, including a co-located monitor, goes with it. A monitor in a different region is the only thing left standing to tell you the site is dark.

5. The box's own internet is broken

The homelab special. Your home ISP drops, the router reboots itself after a firmware update, the dynamic IP rotates and your DNS has not caught up, or the NAT mapping silently expires. The box is on, the app is running, the LAN is happy, and the public internet sees nothing. An external monitor is the only observer that is not on the wrong side of your own front door.

Notice the through-line: in each case a probe on the box can be green while real users get nothing. That gap between a green internal signal and a broken public experience is the whole subject of why uptime monitors miss real outages, and the datacenter case is also why you want an independent record of your provider's downtime when you go to verify your host's uptime claims against an SLA.

A spare Pi is not the fix

The obvious homelab reflex is to add a second box. Put a second Raspberry Pi on the shelf, run the monitor there, and now if the first box dies the second one notices. That is genuinely useful for host-level failures, and you should not feel bad for doing it. But be honest about what it does and does not cover.

A second local box shares almost everything with the first one: the same home internet connection, the same router, the same power strip, the same ISP, the same public IP, the same physical building. If you run two VPS instances in the same datacenter, they share that datacenter's power and network. When the shared thing fails, both boxes fail together, and your second monitor confirms only that the app still answers on the LAN. It cannot confirm that anyone on the public internet can reach you, because it is sitting on the same broken side of the connection. The only observer that can confirm public reachability is one that runs off your network entirely.

So the framing is not external instead of internal. It is external on top of internal. Keep the spare box if you like the host-level redundancy. Add one external probe for the question neither box can answer.

"But my box is behind NAT with no open ports. How would an external monitor even reach it?"

This is the most common objection from people running a homelab, and it is a fair one. If you have not forwarded a port, there is no public address for an outside monitor to hit. You do not have to open one. The standard answer is an outbound-only tunnel, and the most common is Cloudflare Tunnel via the cloudflared daemon. It runs on your box, dials out to Cloudflare, and Cloudflare publishes a public hostname that routes back down that connection to your local service. No inbound port is opened. Your router stays closed. Your app gets a real, public URL.

A minimal public-hostname mapping in a cloudflared config looks like this on your side:

# ~/.cloudflared/config.yml
tunnel: homelab
credentials-file: /home/pi/.cloudflared/homelab.json

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - service: http_status:404

With that running, app.example.com is a public URL backed by your local localhost:8080, and an external monitor can fetch it exactly the way a visitor would, traversing the same tunnel, proxy, and TLS a real request takes. If your app is genuinely private-only and you never want it on the public internet, do not force it. Monitor the public edge you do expose, a reverse proxy, a status endpoint, or a VPN gateway, and keep an internal monitor for the private services behind it.

Monitor the login, not just the landing page

A landing page returning 200 is the weakest possible proof that your self-hosted app works. The part your users actually depend on is the login, and a login can break while the homepage stays perfectly green. A same-host /health probe never sees it, because the process is up and answering; it is the sign-in flow that is broken.

This is where Velprove's differentiator sits. The free, no-code browser login monitor opens a real browser, navigates to your self-hosted Nextcloud, Gitea, Immich, or Home Assistant login, fills in a dedicated test user's credentials, and asserts that a string on the post-login page actually rendered. It signs into your own login the way a person would, catching the broken-login-behind-a-200 case that a status check sails straight past. There is no Playwright script to write; you fill in the URL, the credentials, and the success string in a wizard.

The no-code browser login monitor pointed at a self-hosted Nextcloud login. It signs in as a dedicated low-privilege test user, then Success verification is set to Page contains text on a post-login string, so the check passes only when the sign-in actually worked, not just when the login page returns 200.
Velprove browser login monitor wizard configured against a self-hosted Nextcloud login URL, with a test-user email and password filled in and Success verification set to Page contains text with a post-login string that only renders after a successful sign-in.

Pair that with the free multi-step API monitor when a single request is not enough. On the free plan you get up to 3 steps, which is plenty to chain a token request into an authenticated read and assert on what comes back. Every Velprove monitor, including the free ones, runs from 5 regions you can choose from, one per monitor, so the regional and datacenter cases above are covered by where the check runs, not just what it checks. The multi-step mechanics are walked through in the multi-step API monitoring guide.

Set it up against your own stack in a few minutes

The concrete setup, in order, against your self-hosted stack:

1. HTTP monitor on the public URL. Create an HTTP monitor and point it at your public URL or tunnel hostname. On the verification step, add two Success Conditions: a status code of 200, and a Response Body Contains assertion on a string your correct page always renders. Pick something load-bearing, a known piece of copy or a marker your template emits, so a blank page or a wrong upstream fails the check even when the status code is 200.

2. Browser login monitor on the login. Add a browser login monitor on your self-hosted login URL, fill in the dedicated test user's credentials, and set Success verification to a string that only appears once sign-in succeeds. This is the layer that catches a login regression a homepage check never sees.

3. Optional multi-step API monitor. If your app has an API worth verifying end to end, chain a multi-step monitor: request a token, call an authenticated endpoint, assert on the response. Three steps are available on the free plan.

4. Pick regions and wire alerts. Choose the region closest to your real users for the primary check, and use a second region on another monitor to catch regional DNS or CDN divergence. Add your email for alerts.

On the customer side, a simple /health route and a curl-cron for internal checks are fine to keep. A bare reachability one-liner like curl -fsS https://app.example.com/health || alert from a box on your network tells you the app answers locally, which is useful, and is exactly the LAN-only confirmation the external monitor exists to go beyond.

The HTTP monitor's Success Conditions against a self-hosted app's public URL: Status Code Equals 200 plus a Response Body Contains assertion on a known string. The body assertion fails the check when the reverse proxy serves the wrong upstream or a blank page, even though the status code is still 200.
Velprove new HTTP monitor wizard on the Success Conditions step for a self-hosted app's public tunnel hostname, showing two conditions in order: Status Code Equals 200, and Response Body Contains a known string the correct page always renders.

This is the same external-correctness pattern indie hackers use on side projects that happen to run on managed hosts instead of a closet box, which is laid out in the indie hacker free monitoring stack. The surface differs, the principle does not.

External and internal together: the layered setup

The end state is not one tool. It is two layers that do not overlap. Keep Prometheus, Netdata, or a local Uptime Kuma for what they are good at: resource graphs, container and disk health, internal-only services that never face the public internet, and notification routing inside your network. Those tools see deep into the box, and an external monitor never will.

Then add the external multi-region synthetic for the one thing your internal stack physically cannot do: confirm that the public can reach you. It is the observer that is not on your box, not on your home internet, and not in your datacenter, so it is still up to notice when all of those go dark. For the full cost-and-feature comparison of running Kuma yourself versus a hosted service, the dedicated post is Uptime Kuma vs hosted monitoring. The split this post argues for is simple: internal tools for what is happening inside, one external monitor for whether anyone outside can get in.

Frequently Asked Questions

How do I monitor a self-hosted app from outside my own network?

You point a monitor that runs somewhere other than your network at the public address your app answers on. That public address is either a port you forward through your router, a reverse proxy or VPS that fronts the app, or an outbound tunnel like Cloudflare Tunnel that gives the app a public hostname without opening any inbound ports. The monitor then fetches that URL from one or more regions on a schedule and alerts you when the fetch fails or the page no longer contains the content it should. The point is that the monitor lives off your box and off your home internet, so when your box, your uplink, or your whole site goes dark, the external monitor is still up to notice. With Velprove you create an HTTP monitor on the public URL, add a body assertion on a known string, choose your regions, and wire up email alerts. It is free, with no credit card.

My self-hosted app is behind NAT with no open ports. Can it still be monitored externally?

Yes, if you give it a public hostname. The standard way to do this without opening any inbound ports is Cloudflare Tunnel: you run the cloudflared daemon on your box, it makes an outbound-only connection to Cloudflare, and Cloudflare publishes a public hostname that routes back down that tunnel to your local service. Nothing is exposed on your router, no port is forwarded, yet your app now has a real public URL. You point an external monitor at that hostname and you are monitoring the exact path a real visitor takes. If your app is genuinely private and you never want it on the public internet, then monitor the public edge you do expose, such as a reverse proxy, a status endpoint, or a VPN gateway, and keep an internal monitor for the private services.

Can a free monitor sign into my self-hosted Nextcloud or Gitea login?

Yes. Velprove's browser login monitor opens a real browser, navigates to your login page, fills in a dedicated test user's email and password, and asserts that a string on the post-login page actually rendered. That works against a self-hosted Nextcloud, Gitea, Immich, or Home Assistant login the same way it works against any hosted SaaS, because it drives the same login form a person would. It is no-code: you fill in the URL, the credentials, and the success string in a wizard, with no Playwright script to write. The free plan includes one browser login monitor at a 15-minute interval. Use a low-privilege test account, never your admin credentials, so the monitor can do nothing more than confirm the login works.

Will external monitoring replace Prometheus, Netdata, or Uptime Kuma on my box?

No, and you should not try to make it. The two layers answer different questions. Prometheus, Netdata, and a local Uptime Kuma instance see what is happening inside your box and your network: CPU, memory, disk, container health, internal services, and they route notifications. They are very good at that. What they cannot do, by construction, is tell you whether the public internet can still reach you, because they share fate with the box and the network they live on. An external multi-region monitor answers only that one question and answers it well. Keep your internal tooling for resources and internal services, and add the external monitor for public reachability. For the tool-specific cost and feature comparison, see our Uptime Kuma vs hosted monitoring post.

Is a second Raspberry Pi enough redundancy for monitoring my homelab?

It depends on what failure you want to catch. A second Pi on the same shelf catches the first box crashing, and that is worth something. What it cannot catch is everything the two boxes share: the same home internet connection, the same router, the same power circuit, the same ISP, the same public IP. When your ISP drops, your router reboots, or the power blinks, both Pis go dark together, and the spare confirms only that the app still answers on your LAN, not that anyone on the public internet can reach it. A second local box improves redundancy for host-level failures and does nothing for the failures that share fate with your whole site. Only a monitor that runs off your network can confirm public reachability.

What can an external monitor catch that my app's own /healthendpoint can't?

Everything that happens between your app and a real visitor, plus everything that takes the app itself offline. Your app's /health endpoint runs inside the app process. It can tell you the app thinks it is healthy, but it cannot answer a request once the process has crashed, the host has panicked, the disk is full, the uplink is down, or the datacenter has lost power, because it needs the app to be running to respond at all. An external monitor sees the failure precisely because it is asking from outside: the fetch times out or errors, and it alerts. It also catches a reverse proxy serving the wrong upstream, an expired certificate, a DNS or CDN failure in front of the app, and a login that returns 200 but no longer works. For what a /health endpoint should and should not contain, see our REST API health endpoint guide.

Monitor your self-hosted stack free

The Velprove free plan covers 10 monitors, one no-code browser login monitor that signs into your own self-hosted app, multi-step API monitors up to 3 steps, and 5 regions to choose from, one per monitor. Commercial use is allowed and no credit card is required. Keep your Prometheus, Netdata, or Uptime Kuma for the inside of the box, and add the one external layer that confirms the public can still reach you. Start with the free plan. The first monitor takes a few minutes to set up.

Start monitoring for free

Free browser login monitors. Multi-step API chains. No credit card required.

Start for free