Uptime Kuma Setup - Beautiful Uptime Monitoring
Status: Active
Last Updated: 2026-08-14
Category: Observability - Phase 1: Basic Monitoring
Prerequisites: docker-basics
Time: 1-2 hours
Tags: uptime-kuma, uptime, monitoring, docker, status-page, health-checks
Summary
Hands-on guide to deploying Uptime Kuma, the self-hosted answer to "is it up?" You'll install it with Docker Compose, build HTTP and TCP checks for every service you run, monitor Docker containers directly, and publish a public status page β all in about an hour of work.
π― What You'll Learn
By the end of this article, you'll be able to:
- β Deploy Uptime Kuma with Docker Compose and persistent storage
- β Configure HTTP(S) checks with keywords, status codes, and certificates
- β Monitor raw services with TCP/port checks (databases, game servers, SSH)
- β Watch Docker containers via Uptime Kuma's Docker host integration
- β Publish a branded public status page
- β Set sensible check intervals and retry logic to avoid false alarms
Table of Contents
- What is Uptime Kuma?
- Installation
- Your First Monitor: HTTP Check
- TCP Checks
- Docker Container Monitoring
- Status Pages
- Notifications
- Tuning Intervals and Retries
- Troubleshooting
- Key Takeaways
- Next Steps
What is Uptime Kuma?
Uptime Kuma is a self-hosted uptime checker: it probes your services on a schedule and tells you when one stops answering. It deliberately answers only Rung 1 of the observability ladder ("is it alive?") but does so extremely well.
What it's great at:
- Fast detection that an endpoint stopped responding (down to ~20s intervals)
- Dozens of check types: HTTP, TCP port, ping, DNS, Docker containers, even MQTT and game servers
- Push monitors β your cron jobs call Kuma to say "I ran successfully"
- A gorgeous, shareable status page out of the box
What it doesn't do (that later phases add):
- No historical metrics graphs beyond response-time charts
- No system-level metrics (CPU/RAM/disk) β use Netdata or Node Exporter
- No log aggregation β that's Loki's job in Phase 4
Think of it as the smoke detector of your infrastructure: simple, always watching, loud when needed.
Resource Requirements
| Component | Requirement |
|---|---|
| RAM | ~100β256 MB |
| CPU | Negligible |
| Disk | Small β history is lightweight |
| Network | Outbound to every monitored target |
Installation
Directory Layout
mkdir -p ~/apps/uptime-kuma && cd ~/apps/uptime-kuma
Keeping each app in its own directory under ~/apps/ makes backups and migration trivial β copy the folder, restore the folder.
Docker Compose File
Create compose.yaml:
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- ./data:/app/data
Notes:
louislam/uptime-kuma:1pins major version 1 β upgrades within v1 are safe; pinning prevents surprise breaking changes from a floatinglatest../data:/app/dataholds the SQLite database, uploaded certificates, and settings. This directory is the entire state of your instance β back it up and you can rebuild anywhere.- Port
3001is Kuma's default web UI. If occupied, change the left side only (3002:3001).
Launch It
docker compose up -d
What happens:
1. Docker pulls louislam/uptime-kuma:1 (~200 MB)
2. Creates container "uptime-kuma"
3. Mounts ./data into the container
4. Starts the Node.js server listening on :3001
5. restart: unless-stopped ensures it survives reboots
Verify:
docker compose ps # STATUS should be "Up"
curl -I http://localhost:3001 # expect HTTP 200
Now open http://<server-ip>:3001, create the admin account, and set the timezone correctly β timestamps on incidents and heartbeats depend on it.
π Security note: The UI has no second factor by default and full control over notifications. Don't expose port 3001 to the internet without a reverse proxy + auth, or keep it LAN/VPN-only (see kb/security/wireguard-vpn).
Your First Monitor: HTTP Check
Log in β Add New Monitor. The default type is HTTP(s).
Basic Website Check
| Field | Value | Why |
|---|---|---|
| Friendly Name | fogserv.cloud homepage |
Shown everywhere; be specific |
| URL | https://fogserv.cloud |
The exact thing users hit |
| Heartbeat Interval | 60 s | How often to probe |
| Retries | 2 | Down only after 3 consecutive failures |
| Retry Interval | 60 s | Gap between retries |
Save, and within a minute you'll see green heartbeats. Each heartbeat records status + response time + cert expiry.
What a heartbeat actually does:
every 60s:
GET https://fogserv.cloud
ββ DNS resolves?
ββ TCP connects?
ββ TLS handshake OK?
ββ HTTP response received?
ββ response time recorded
any failure Γ(retries+1) β mark DOWN β fire notifications
back UP later β send recovery notification
Level Up the Check
A 200-response isn't proof the site works. Harden it:
Keyword check β verify real content came back:
- Enable Keyword and enter a string that must appear, e.g.
FogServ - A hijacked or broken page returning 200 with garbage now fails the check
Expected status code β if your app redirects / to /login when healthy, set expected code to 302.
Certificate expiry β Kuma tracks TLS expiry automatically. Add a second monitor of type HTTP(s) - Keyword with "Ignore TLS/SSL error" off, or just rely on the certificate-expiry notification setting under Settings β Notifications. No more surprise cert outages.
Recommended First Five Checks
For a typical self-hosted box:
- Main website (HTTP + keyword)
- API endpoint (
https://api.example.com/healthz) - Database (TCP β below)
- Reverse proxy / Traefik dashboard
- Backup completion (Push monitor β see Notifications)
TCP Checks
Many services have no HTTP interface: PostgreSQL on 5432, Redis on 6379, Minecraft on 25565, SSH itself. For those, use monitor type TCP Port.
| Field | Example value |
|---|---|
| Friendly Name | postgres on db01 |
| Hostname | db01.lan |
| Port | 5432 |
What happens per heartbeat:
connect(db01.lan:5432)
ββ success (<timeout) β UP
ββ timeout/refused β count failure toward DOWN
TCP Check Caveats
- "Port open" β "service healthy." Postgres will accept connections while deadlocked. TCP proves the process listens; deeper health needs exporters (Phase 2) or app-level
/healthz. - Timeout tuning: default 10 s is fine on a LAN; raise it for slow WAN targets.
- Monitor the user path, not the internal path. If users reach Postgres through pgbouncer on 6432, check 6432 β checking 5432 directly can show UP while the actual route is broken.
- DNS name vs IP: using a hostname also detects resolver failures β usually what you want.
Ping Monitors
Type Ping (ICMP) checks raw reachability, bypassing services entirely. Great for distinguishing "network down" from "service down": if ping db01 succeeds but TCP 5432 fails, the network is fine and Postgres is the suspect.
β οΈ Inside Docker, ping requires
NET_RAW; if ping monitors fail immediately in the container, either grant the capability or use TCP checks instead.
Docker Container Monitoring
Uptime Kuma can watch Docker containers directly β catching the crash-restart loop that never takes a port down long enough to fail a TCP check.
Step 1: Create a Read-Only Docker User
Kuma talks to the Docker socket, which is root-equivalent. Never mount /var/run/docker.sock read-write into a monitoring tool. Instead create a socket-proxy or a restricted user.
Simplest safe option β a group-restricted user via Docker socket proxy (tecnativa/docker-socket-proxy):
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
container_name: docker-socket-proxy
restart: unless-stopped
environment:
CONTAINERS: 1 # allow ONLY container listing
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- kuma-net
uptime-kuma:
# ... as before ...
environment:
- DOCKER_HOST=tcp://socket-proxy:2375
networks:
- kuma-net
networks:
kuma-net:
What this buys you: the proxy exposes only the GET /containers API. Even if Uptime Kuma were compromised, the attacker cannot spawn containers or touch the daemon β they can list containers, nothing more.
Step 2: Add a Docker Host
In Kuma: Settings β Docker Hosts β Add:
- Name:
local docker - Type:
tcp - URL:
socket-proxy:2375
Test & save.
Step 3: Add Container Monitors
Add New Monitor β type: Docker Container:
- Docker Host:
local docker - Container name:
nginx-proxy(exact container name) - Interval: 60 s
The monitor flips DOWN if the container stops, and recovers automatically when it starts again. Repeat for every critical container.
Status Pages
One of Kuma's killer features: a public page showing service health, no login required.
Add New Status Page β choose slug (status.example.com/s/my-status) β drag monitors into groups:
π Public Services
βββ fogserv.cloud homepage
βββ API endpoint
π Internal (optional β hide these)
βββ postgres on db01
βββ backup daily push
Options worth enabling:
- Show powered-by footer off for clean branding
- Custom theme/logo/CSS to match your site
- Incident history β post updates during outages ("Investigatingβ¦", "Identifiedβ¦", "Resolved") directly on the page
Why this matters: during an incident, a status page absorbs the "is it just me?" traffic that would otherwise hammer your support channels and your already-sick servers. Publish the URL in your email footer or docs before you need it.
π‘ Keep truly internal targets (databases, VPN endpoints) off the public page. Use a separate private status page for those.
Notifications
Checks are useless unless someone hears about failures. Uptime Kuma supports 90+ notification channels β the Phase 1 next lesson covers alert design in depth (simple-alerts); here's the quick setup for the three most common.
Setup Pattern (same for all channels)
- Settings β Notifications β Setup Notification
- Choose type (SMTP Email / Discord / Telegram β¦)
- Fill credentials/webhook
- β Enable "Apply on all existing monitors"
- Click Test β confirm the message actually arrives before trusting it
SMTP Email
| Field | Example |
|---|---|
| Host | smtp.fastmail.com |
| Port | 465, TLS on |
| User/Pass | mailbox credentials |
| To | ops@yourdomain.com |
Discord Webhook
Server Settings β Integrations β Webhooks β New Webhook β Copy URL β paste into Kuma. Optionally set a custom username like Uptime Bot and enable the rich embed format.
Push Monitors for Cron Jobs
Reverse-direction check: instead of Kuma asking "did it happen?", your job reports "I succeeded":
- Add monitor of type Push, note its URL
- Append to your backup script:
#!/bin/bash
restic backup /data \
&& curl -fsS --max-time 10 "http://uptime-kuma:3001/api/push/AbCdEf123?msg=ok&ping=$SECONDS" \
|| echo "ALERT: backup or push failed" >&2
How this catches silent death: if the cron job never runs, no push arrives, and after the grace period Kuma marks the monitor DOWN. This is the only reliable way to detect jobs that didn't run β their absence produces no error anywhere else. Use it for backups, cert renewals, and report generation.
Tuning Intervals and Retries
Default settings can produce both missed events and false alarms. Tune deliberately:
| Setting | Too aggressive | Sweet spot | Too lax |
|---|---|---|---|
| Heartbeat interval | β€20 s (noisy, hammers targets) | 60 s | β₯5 min (slow MTTD) |
| Retries | 0 (flaps on one lost packet) | 2β3 | 5+ (masks short outages) |
| Retry interval | same as main (blast) | 60 s | β |
Effective worst-case detection time:
MTTD β interval + retries Γ retry_interval
60s + 2Γ60s = up to ~3 min from failure to notification
Three minutes is excellent for free. Going below ~20 s intervals mostly measures network jitter, not availability.
Anti-Flap Tips
- Flapping = rapid UP/DOWN cycling, which spams notifications and erodes trust.
- Increase retries before decreasing intervals.
- For flaky WAN targets, prefer "down after N minutes of continuous failure" semantics via high retry counts rather than instant paging.
- Kuma shows heartbeat history per monitor β investigate any monitor that flips more than once a week; either the target or the check is wrong.
Troubleshooting
Monitor shows DOWN but curl from the server works
- Kuma runs inside a container β it may not resolve LAN names or reach the same network. Test from inside:
docker exec uptime-kuma curl -v https://target. - IPv6 vs IPv4 mismatch: force the expected family in the URL or DNS.
Certificate errors on internal HTTPS
Self-signed certs fail validation. Either trust your internal CA in the container, or consciously enable "Ignore TLS/SSL error" β but then you lose expiry alerts for that monitor; add a separate cert-check approach.
Docker container monitor can't connect
DOCKER_HOSTunset or wrong β check the env var and that both containers share the network.- Socket proxy returns 403 β the environment variable for that verb (e.g.,
CONTAINERS=1) isn't enabled on the proxy.
Lost everything after redeploying
You mounted no volume (or mounted the wrong path). State lives in ./data. Restore = stop container, restore ./data, start. Automate: include ~/apps/uptime-kuma/data in your restic/borg backup set, and consider a Push monitor on that backup job too β monitor the monitor.
Notification never arrived on test
Check container logs (docker compose logs uptime-kuma) for the delivery attempt; most failures are wrong SMTP port/TLS mode or Discord webhook URLs copied with trailing spaces.
Key Takeaways
- Uptime Kuma = fast, cheap Rung-1 observability: deploy in 10 minutes, ~100 MB RAM.
- Persist
./dataβ it is the whole instance; back it up like any database. - Harden HTTP checks with keywords/status codes so a lying 200 still fails.
- Use TCP checks for non-HTTP services, ping to separate network vs service faults.
- Never expose the raw Docker socket β use a read-only socket proxy.
- Push monitors catch jobs that silently never ran β apply them to backups first.
- Tune interval/retries for ~3-minute worst-case detection and anti-flap stability.
- Publish a status page before the incident, not during it.
π Related
- Next lesson: netdata-basics β add real-time system metrics to the picture
- Also in Phase 1: why-monitor, simple-alerts
- Course overview: README
- Related KB sections:
- kb/containers/docker-basics β prerequisite for the deployment above
- kb/security/tls-configuration β cert handling behind proxies
- kb/sysadmin/secrets β where to store SMTP/notification credentials