Skip to content
← All posts
· 7 min read· By

Uptime Kuma: Self-Hosted Status Monitoring with Docker Compose

Set up Uptime Kuma v2.5.5 with Docker Compose: HTTP, keyword and push monitors, Telegram alerts, and WebSocket-correct operation behind Traefik.

Uptime KumaDockerMonitoringSelf-HostingTraefikTelegramDevOps

When a service goes down, in the worst case you hear about it from a customer. UptimeRobot cut its free plan in 2024 to 50 monitors with 5-minute intervals; BetterStack gets noticeably expensive past the fifth monitored service. Uptime Kuma provides the same core features for self-hosted infrastructure with no subscription. If you already run Docker Compose on your VPS, you'll have the stack up in under ten minutes; the downtime cost calculator on this site helps you quickly put a number on what an outage actually costs.

What Uptime Kuma Adds Beyond a Simple Ping Check

An ICMP ping only confirms that a host responds. Uptime Kuma also verifies HTTP status codes, searches the response body for a specific keyword, and warns you before TLS certificates expire. Over 90 notification channels are built in — including Telegram, Slack, email, and generic webhooks.

Supported Monitor Types at a Glance

Type What is checked Typical use case
HTTP(s) Status code 2xx/3xx Website, REST endpoint
HTTP(s) Keyword Status code and body content CMS, login page
TCP Port Connection establishment Database, SSH
Ping ICMP echo Router, bare-metal server
DNS Record Resolved value A record, DMARC entry
Push Waits for heartbeat request Cron job, background worker
Docker Container Container status via socket Your own Compose stacks

The Push Monitor: Inverting the Logic

Classic polling monitors miss the case where a job simply stops starting — a service that silently ceases to run stays invisible. Push mode reverses the direction: Uptime Kuma waits for a signal that never arrives and fires the alert once the configured interval is exceeded. The push URL looks like /api/push/<token>?status=up&msg=OK&ping= and can be called via curl from any cron job or script.

Setting Up the Stack: the Compose File and the SQLite Problem

The official image is on Docker Hub at louislam/uptime-kuma. The 2 tag always points to the latest stable v2.x release — currently 2.5.5. The latest tag, by contrast, includes unstable development builds; the official Docker Tags wiki explicitly recommends against it for production. This is a deliberate choice: in the Uptime Kuma project, latest is the rolling-development track, not the release track — the opposite of the usual convention.

The Minimal Compose File

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    volumes:
      - uptime-kuma:/app/data
    ports:
      - "3001:3001"
    environment:
      - TZ=Europe/Berlin
    restart: always

volumes:
  uptime-kuma:

All application state lives in /app/data inside the container — that's where Kuma stores the SQLite database kuma.db, all configuration, and the heartbeat history.

Why a Named Volume Instead of a Bind Mount

If you map /app/data to a host directory instead, you may encounter the following error on slow storage — SD cards, NFS mounts, certain VPS storage backends:

[ERROR] SQLITE_BUSY: database is locked

The cause: Uptime Kuma opens the database in WAL mode and writes heartbeat data at short intervals. On high-latency media, these writes overlap until SQLite exceeds its built-in timeout. Named Docker volumes live under /var/lib/docker/volumes/ on the local host filesystem and reliably avoid this bottleneck. If you prefer a bind mount for backup reasons, make sure the target directory is owned by UID 1000 before first start:

mkdir -p ./kuma-data
chown 1000:1000 ./kuma-data

Then use ./kuma-data:/app/data in the Compose file.

Setting Up Monitors and Wiring Telegram Notifications

After the first start at http://localhost:3001, Kuma guides you through creating an admin account. The approach mirrors the pattern in Docker Compose: Multi-Service Setup with Healthchecks and a Custom Network: get the stack running first, then wire up monitoring and dependencies.

HTTP Monitor with Keyword Verification

For a simple website, the HTTP(s) type is sufficient. Choose HTTP(s) Keyword when you need to confirm that not only the status code is correct, but the body actually contains the expected content. This is especially useful when your application also returns HTTP 200 in maintenance mode — without keyword checking, the outage would go unnoticed.

Telegram Alerts: Token, Chat ID, and Test

  1. Open Telegram, search for @BotFather, and send /newbot. You receive a token in the format 123456789:ABCdefGHI....
  2. Send your new bot a message so that a chat object is created.
  3. Call https://api.telegram.org/bot<TOKEN>/getUpdates and read the chat.id from the JSON response. For group chats this value is negative — the minus sign must be included.
  4. In Uptime Kuma: Settings → Notifications → Add → Telegram. Enter the token and chat ID, then click Test.

At idle with 15 active monitors (interval: 60 seconds), memory usage on our VPS is around 65 MB RSS, measured with:

docker stats --no-stream --format "{{.MemUsage}}" uptime-kuma

This makes Uptime Kuma viable even on small 1 GB instances that already host Traefik as a reverse proxy and several other services.

Running Behind Traefik: Getting WebSocket Right

Uptime Kuma's frontend communicates exclusively via Socket.IO, which uses WebSocket under the hood. Without correct proxy configuration, after login you'll see a grey screen with "Cannot connect to the socket server" — the browser loads HTML, but the WebSocket connection fails at the proxy.

Client                  Traefik                 Uptime Kuma
  |                        |                         |
  |--- GET /          ---> |--- GET /          ----> |
  |<-- 200 HTML       ---  |<-- 200 HTML       ----  |
  |                        |                         |
  |--- WS Upgrade     ---> |--- WS Upgrade     ----> |
  |<-- 101 Switching  ---  |<-- 101 Switching  ----  |
  |                        |                         |
  |<===== Socket.IO frames ========================> |

Traefik Labels for a Single Instance

Traefik forwards Upgrade and Connection headers by default, so no custom WebSocket middleware is needed. Sticky sessions would only be required when running multiple Uptime Kuma replicas — unnecessary for a single instance.

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    volumes:
      - uptime-kuma:/app/data
    environment:
      - TZ=Europe/Berlin
    restart: always
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.uptime-kuma.rule=Host(`status.example.com`)"
      - "traefik.http.routers.uptime-kuma.entrypoints=websecure"
      - "traefik.http.routers.uptime-kuma.tls.certresolver=letsencrypt"
      - "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"

volumes:
  uptime-kuma:

networks:
  proxy:
    external: true

Enable "Trust Proxy"

In Settings → Reverse Proxy → Trust Proxy, set the option to Yes so that Uptime Kuma accepts the X-Forwarded-* headers from the proxy. Without this, all log entries show the internal Docker IP of the Traefik container instead of the real client IP.

Resource Consumption and Operational Limits

Measured Values on a VPS

Setup: 15 monitors (10 HTTP, 3 TCP, 2 Push), 60-second interval, VPS with 2 vCPUs (AMD EPYC), 4 GB RAM.

Metric Measured value
RSS idle ~65 MB
CPU idle < 0.1 %
Time to first poll after start ~4 seconds
Image size (louislam/uptime-kuma:2) ~160 MB compressed

The image is based on Node.js 22 on Debian Bookworm Slim. A 2-slim tag is available for environments where disk space is tight.

When Uptime Kuma Hits Its Limits

Above roughly 200 monitors with polling intervals under 30 seconds, the SQLite write bottleneck can reappear. At that scale, switching to Prometheus + Grafana is the right move. For a typical self-hosting environment with fewer than 100 services, Uptime Kuma is more than sufficient. Container-level health checks as a complementary layer are covered in Docker HEALTHCHECK and restart_policy: when containers heal themselves. For regular backups of the Kuma volume, Restic with encrypted backups to S3 is a natural companion.

Questions from the Field

Does Uptime Kuma run on a Raspberry Pi? Yes. The official image is available for both amd64 and arm64. On a Pi with an SD card, set the environment variable UPTIME_KUMA_SQLITE_SINGLE_CONNECTION=1 to avoid SQLITE_BUSY errors from concurrent writes.

How do I back up the Kuma database? Mount the named volume into a temporary container and archive the directory with Restic. The volume contains only the SQLite file and stays small.

Can I run Uptime Kuma without a public domain? Yes. Without Traefik, Kuma is accessible directly at http://<server-ip>:3001. Telegram notifications work regardless, as long as the host can make outbound HTTPS connections to the Telegram Bot API.

What happens if I lose my bot token? Telegram bot tokens don't expire. Use @BotFather → /mybots → Regenerate token to get a new one; then update it in Kuma's notification settings.

Is there a public status page for external users? Yes. Under Status Page you can create a publicly accessible — or password-protected — page that shows the state of your services without a login. It can be served under its own subdomain.

What Comes Next

The complete installation guide and all configuration options are in the official wiki. The reverse proxy section additionally covers nginx, Caddy, and Apache alongside Traefik. For deeper metrics — CPU histograms, latency dashboards, AlertManager integration — Prometheus + Grafana is the logical next step; a dedicated post on that stack is coming soon. For questions about concrete infrastructure architecture, we're happy to help through our IT support service.

Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the Impressum).

Discuss your project