DEV Community

Cover image for Uptime Kuma Docker Setup: What I Change Before Trusting It
Qasim Parray
Qasim Parray

Posted on Originally published at abrarqasim.com

Uptime Kuma Docker Setup: What I Change Before Trusting It

I found out a client's site had been down for six hours the way nobody wants to: the client told me. A WhatsApp message at breakfast, one line, no greeting. The server was fine. A certificate renewal had failed quietly, the browser was throwing a full-page warning, and my only monitoring was me occasionally opening the site and thinking "looks okay."

That was the week I stopped treating monitoring as a someday task. I wanted something I could host myself, that cost nothing per monitor, and that I could set up in an evening. Uptime Kuma fit. It is also very easy to install badly, which is the part most tutorials skip.

Short version for the impatient: the default Docker setup works, but I change four things before I trust it with a client's name on the status page. Where it runs, who can reach port 3001, how I get alerted when the monitor itself dies, and what I do with the data folder. The rest of this post is those four, in the order I get bitten by them.

What the default compose file gives you

The project's README shows the fastest path: make a folder, download a compose.yaml, run docker compose up -d. I checked the file as it stands today and it is short.

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    ports:
      - "3001:3001"
Enter fullscreen mode Exit fullscreen mode

That's the whole thing. One container, one bind mount for state, one port. It pins to the major version 2 tag, so you get minor updates when you pull but not a surprise jump to a different major release. I like that default. I have been burned by latest before, and not only with this tool.

What you get on first launch is a pleasant surprise if you have used paid monitors. HTTP and keyword checks, TCP, ping, DNS, a push monitor for cron jobs, and a Docker container monitor. Check intervals go down to 20 seconds. Notifications can go to Telegram, Discord, Slack, email and, according to the README, more than 90 other services. Multiple status pages, each mappable to its own domain. That covers what most small agencies pay a monthly fee for.

The setup is fine for a laptop. It is not fine for a VPS with a public IP, and the reason is on the ports line.

Port 3001 is open to the world

Writing "3001:3001" in a compose file binds the port on every network interface. The README says as much: Uptime Kuma is now running on all network interfaces. On a home machine behind a router, nobody cares. On a Hetzner box with a public address, you have just put an admin login on the internet at a predictable port, and the first thing the setup screen asks you to do is create the admin account.

I have seen people leave that screen open for a day. Whoever loads it first becomes the admin. That is a bad race to run.

The README mentions the fix itself: limit exposure to localhost. In compose terms that is one extra prefix.

    ports:
      - "127.0.0.1:3001:3001"
Enter fullscreen mode Exit fullscreen mode

Now the container only answers on the loopback interface. To reach the dashboard you need something in front of it. I use a reverse proxy with automatic certificates, and if you are choosing between the common options, I wrote up how Caddy and nginx compare on a small VPS. The Caddy config for this is two lines: the hostname, then a reverse_proxy 127.0.0.1:3001. Done.

If you would rather open no inbound ports at all, a tunnel works too. I covered that route in the Cloudflare Tunnel post. Either way, the dashboard belongs behind a hostname with TLS, not an IP and a port number.

One more thing on the first-run screen. Create the admin account immediately, pick a long passphrase, and turn on two-factor auth. The README lists 2FA as a built-in feature, so there is no excuse. It takes a minute and it removes the most likely way someone else ends up owning your monitor.

The monitor cannot watch itself die

This is the mistake I made first, and it is the one that matters most.

If Uptime Kuma runs on the same server as the sites it watches, then a dead server takes out both the sites and the thing that was supposed to tell you. Your phone stays silent. Silence looks exactly like "everything is fine." I only noticed my version of this because a test outage produced zero alerts, which was embarrassing, since I had caused the outage on purpose.

There are two fixes, and I use both.

The first is location. Run Uptime Kuma on a different provider or at least a different machine from the thing it watches. A cheap second VPS in another datacenter is enough. It does not need much: the process is a single Node app with a small database behind it, so it will sit comfortably on the smallest plan most providers sell. If your sites are on one host, put the monitor somewhere else. That is the entire rule.

The second is a heartbeat going the other way. Uptime Kuma has a push monitor type: instead of the tool calling out to a URL, something calls in to a URL it generates, and if the call stops arriving, the monitor goes red. I point a tiny cron job at it from the box that runs Uptime Kuma, and I watch that push monitor from a free external ping service. Now if the monitor machine dies, something else notices. It sounds like turtles all the way down, but you only need one level of it. A free tier at an outside service is plenty for a single check.

A smaller version of the same mistake: alerts that go to an email address on the domain you are monitoring. When the mail server is down along with everything else, the alert never lands. Send to Telegram, or a personal address on a different domain. I use a Telegram bot for anything urgent because the notification arrives on my phone within seconds and I cannot sleep through it as easily as an email.

Tuning the checks so you trust the alerts

A monitor that cries wolf gets muted. A muted monitor is worse than none, because you think you have coverage.

My first week I left every check at the lowest interval and every alert on the first failure. A single dropped packet paged me at 3 a.m. The fix is dull. Set retries to 2 or 3 before a monitor flips to down, and keep the interval at a minute for client sites, longer for anything internal. A 20-second interval is available, and I use it for exactly one thing, a payments endpoint where thirty seconds of downtime has a price.

Check what the monitor actually checks, too. An HTTP monitor that accepts any 2xx will happily report "up" for a site that now serves a blank page or a maintenance banner. For client sites I switch to a keyword monitor and look for a string I know sits in the footer or the page title. If the string disappears, the page broke, even when the server still answers 200. That one change caught a broken deploy for me before the client noticed, which made up for the six hours of embarrassment.

Certificates deserve their own monitor. Uptime Kuma shows certificate info and can alert before expiry. Turn it on for every HTTPS check. It would have saved me that breakfast message.

Backups and updates for the data folder

Everything the tool knows lives in the ./data directory from the compose file: the database, your monitors, your notification settings. Lose it and you rebuild by hand from memory. The README also warns that network file systems like NFS are not supported for this path, so keep it on local disk or a normal volume.

I back that folder up with the same job that handles the rest of my small servers, a nightly copy off the machine. If you have not set one up, the Docker Compose services reference is where I would start for understanding how the bind mount and restart policy fit together. The unless-stopped policy in the default file means the container comes back after a reboot, but it will stay down if you stopped it on purpose. That is the behavior I want.

For updates I do it manually and on a schedule. Pull the new image, recreate the container, look at the dashboard, done. The project keeps update instructions on its wiki, and I read the release notes before any major jump. Because the compose file pins to the major tag, a routine pull stays inside version 2.

Do one restore test, even a rough one. Copy the data folder to a scratch machine, start a container against it, and confirm your monitors show up. Mine took ten minutes and found that I had forgotten to include the folder in the backup job at all. I would much rather learn that on a Tuesday afternoon than during an outage.

What I would do this week

If you run client sites and have no monitoring, set up Uptime Kuma on a machine that is not the one hosting those sites. Bind it to localhost, put it behind a hostname with TLS, and create the admin account with 2FA before you do anything else. Add one keyword monitor and one certificate check per site, then route alerts to Telegram.

Then break something on purpose. Stop a site, wait for the alert, and time how long it takes to reach your phone. If it never arrives, you just found the real work. I do this kind of setup for small teams as part of my freelance engineering work, and the failed test alert is the single most common thing I find on day one.


Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.

Top comments (0)