Short answer: run Uptime Kuma 2 with Docker Compose from the image louislam/uptime-kuma:2, publish it on 127.0.0.1:3001 only, keep /app/data in a named volume, and put Nginx or Caddy with a Let’s Encrypt certificate in front of it, passing the WebSocket headers and the original Host. Then create the admin account, add monitors and alerts, and back up the volume. Run it on a different server from the ones it watches, or it goes down with them and tells you nothing.
Everything below was checked against Uptime Kuma 2.5.6, released on 9 October 2026: its compose file, the configuration of the published Docker images, its source code and the project wiki. The commands are for Ubuntu 24.04 and Debian 12 or 13 with Docker Engine and the compose plugin.
What you need, and which image tag to use
- A small server of its own. A monitor needs little CPU and memory. Even the smallest Standard KVM plan (2 cores, 4 GB RAM, 20 GB disk) runs it next to Nginx with room to spare.
- Docker with the compose plugin. Install Docker on Ubuntu or Debian from Docker’s repository;
docker compose versionmust print a version. If it says the command does not exist, see docker compose: command not found. - A subdomain such as
status.example.comwith an A record (and AAAA if the server has IPv6) pointing at the server, as in pointing a domain at your VPS. Uptime Kuma does not work under a sub-path likeexample.com/uptime. - Ports 80 and 443 open in the firewall. Port 3001 stays closed.
The tags on Docker Hub, as published for 2.5.6:
| Tag | What it contains | Use it when |
|---|---|---|
2 | Latest 2.x on Node.js 22, with embedded MariaDB and Chromium; about 600 MB compressed (amd64) | The default the project recommends |
2-slim | The same without MariaDB and Chromium; about 180 MB compressed | You use SQLite or an external MariaDB and no Browser Engine monitors |
2.5.6 | Pinned to one release (also 2.5.6-slim) | You want upgrades only when you edit the file |
2-rootless | Runs as user node (UID 1000) | The data directory is owned by 1000:1000; not for upgrades from 1.x |
latest, 1 | The old 1.x line on Node.js 18 | Never for a new install: the project marks latest as deprecated |
latest is the trap: an image name without a tag pulls 1.x, not 2. The project wiki says so, and the digests on Docker Hub confirm that latest and 1 are the same image.
The Docker Compose file
sudo mkdir -p /opt/uptime-kuma
cd /opt/uptime-kuma
sudo nano compose.yamlservices:
uptime-kuma:
image: louislam/uptime-kuma:2
container_name: uptime-kuma
restart: unless-stopped
ports:
# localhost only: Nginx or Caddy publishes it with TLS
- "127.0.0.1:3001:3001"
volumes:
- kuma-data:/app/data
volumes:
kuma-data:
name: uptime-kuma-datasudo docker compose up -d
sudo docker compose ps
sudo docker compose logs -fThe file validates against the Compose specification. Three lines in it matter more than they look:
127.0.0.1:3001:3001instead of the3001:3001in the official example. Docker writes its own firewall rules, so a published port is reachable from the internet even when UFW denies it (why). And a fresh Uptime Kuma lets whoever opens it first create the admin account.- The named volume. All state lives in
/app/data: the SQLite filekuma.db,db-config.json, uploads and, if you choose it, the embedded MariaDB. The 2.x images declare no volume of their own, so without this line the data disappears with the container. Network file systems such as NFS are not supported; keep the volume on local disk. name: uptime-kuma-datafixes the volume name, so the backup commands below work whatever the directory is called.
docker compose ps shows (health: starting) at first: the image checks itself every 60 seconds and allows 180 seconds to start. It turns to (healthy) once the web server answers.
Nginx in front: HTTPS and WebSockets
The dashboard talks to the server over one long-lived Socket.IO connection, which has to be upgraded to a WebSocket through the proxy. Put this into /etc/nginx/sites-available/status.example.com; we tested it with nginx -t on Nginx 1.22 (Debian 12):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name status.example.com;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
# WebSocket upgrade for the live dashboard
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Uptime Kuma compares Origin with Host on every WebSocket
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/status.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d status.example.com --redirectCertbot switches the server block to HTTPS and adds the redirect from HTTP; installing it and renewal are covered in the Nginx reverse proxy guide. Do not drop the Host line. Uptime Kuma checks every WebSocket request and refuses it when the host in the browser’s Origin header differs from the Host header it receives. Without the line, Nginx sends the address from proxy_pass as the host, the dashboard never gets its WebSocket and stays on slower HTTP polling, and docker compose logs fills with lines containing this message (the address at the end is the client as Uptime Kuma sees it):
[AUTH] ERROR: Origin (https://status.example.com) does not match host (127.0.0.1:3001), IP: 172.18.0.1After the first login, set two things in Settings. Under Reverse Proxy, set Trust Proxy to Yes so the logs show visitors’ addresses instead of the proxy’s; that is safe only because port 3001 answers on localhost alone. Under General, set Primary Base URL to https://status.example.com, which push URLs and links in alerts are built from.
Or Caddy, in three lines
status.example.com {
reverse_proxy 127.0.0.1:3001
}Caddy fetches and renews the certificate itself, passes the Host header through unchanged, sets X-Forwarded-For and X-Forwarded-Proto, and proxies WebSockets without extra settings. Apply it with sudo systemctl reload caddy. Ports 80 and 443 must be reachable from the internet for its certificate challenges.
First start: database and admin account
- 1Choose the database
Open
https://status.example.com. Version 2 first asks “Which database would you like to use?”: SQLite, the embedded MariaDB (full image only) or an external MariaDB. For a personal or small-team monitor take SQLite, one file that the setup page itself recommends for small deployments. - 2Create the admin account at once
The next page is “Create your admin account”. Until you submit it, anyone who reaches the page can do it instead, so do it as soon as the site answers, or before that through the SSH tunnel below.
- 3Turn on two-factor login
Settings, Security, 2FA Settings, Enable 2FA. The dashboard can pause monitors and read every notification secret you store in it.
To do all this before DNS and the certificate are ready, open an SSH tunnel and use http://localhost:3001 on your own computer. How -L works is in SSH local port forwarding. A forgotten password is reset from the server:
ssh -L 3001:127.0.0.1:3001 USER@SERVER_IPcd /opt/uptime-kuma
sudo docker compose exec uptime-kuma npm run reset-passwordMonitors worth adding first
| Monitor type | Point it at | What it catches |
|---|---|---|
| HTTP(s) - Keyword | A page, with a word that only appears when it works | A 200 response that is really an error page or an empty template |
| HTTP(s) | Site or health endpoint | Status codes outside 200-299, timeouts; tick Certificate Expiry Notification, which is off by default |
| TCP Port | SSH, mail or game server ports | A service that stopped listening, or a firewall change |
| DNS | Your domain's A or NS record | A record that stops resolving; with a condition on the expected value, also one that changed |
| Ping | The server itself | The host is unreachable; says nothing about the services on it |
| Push | Cron jobs and backups | A job that did not run: Kuma alerts when its URL is not called in time |
New monitors check every 60 seconds with Retries set to 0, the default in current 1.23.x and 2.x releases, so a single failed check raises an alert. Set retries to 1 or 2 on anything that pages you. Each new monitor also has Domain Name Expiry Notification switched on: Uptime Kuma looks the domain up over RDAP, for the TLDs that offer it, and warns 7, 14 and 21 days before it expires (the days are set under Settings, Notifications). For a Push monitor, Kuma shows the URL to call; append it to a backup script so the call only happens on success:
/usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 "https://status.example.com/api/push/PUSH_TOKEN?status=up&msg=OK&ping=" > /dev/nullInside the container, localhost means the container itself, so a monitor for http://localhost:8080 checks nothing on the host. Use the address the service is reachable at from outside. The Docker Container monitor type needs the Docker socket mounted into the container, which hands it root on that host; on a monitor that watches other servers, check their services over HTTP or TCP instead.
Before planned work, open Maintenance in the profile menu, next to Settings, choose Schedule Maintenance and pick the affected monitors: while it runs they show Maintenance instead of down, and no alert goes out.
Alerts by Telegram, Discord and email
Notifications are set up under Settings, Notifications, Set Up Notification, and attached to monitors. Tick Default enabled for new monitors and Apply on all existing monitors for the ones you have, then press Test before you rely on it.
- Telegram: create a bot with @BotFather and paste its token into Bot Token. Send the bot one message, then press Auto Get, which fills in the Chat ID.
- Discord: in your server, open Server Settings, Integrations, View Webhooks, New Webhook, copy the URL and paste it into Discord Webhook URL.
- Email (SMTP): Hostname, Port, Security, Username, Password, From Email and To Email of a mail provider you already use: port 587 with Security set to None / STARTTLS, or 465 with TLS. A monitor should not depend on a mail server it watches.
Version 2 renders email subjects and bodies with LiquidJS: variable names such as name, msg and status are case-sensitive, and unknown variables are silently left out, so templates copied from 1.x may need fixing.
A public status page
Status Pages, New Status Page asks for a name and a slug; the page appears at https://status.example.com/status/SLUG. Add monitor groups and they show their state and uptime bars without a login. To serve it on a domain of its own, enter that domain under Domain Names on the page, point its DNS at the server, add it to server_name and run certbot for it too. Everything on a status page is public, including monitor names, so name them for visitors.
Back up the data volume
Version 2 dropped the JSON export: the data directory is the only supported backup. In SQLite mode Uptime Kuma runs the database with a write-ahead log, so a file copy taken while it writes can miss data. Stop the container for the few seconds the archive takes:
cd /opt/uptime-kuma
VOL=$(sudo docker volume inspect -f '{{ .Mountpoint }}' uptime-kuma-data)
sudo docker compose stop
sudo tar -czf /root/uptime-kuma-$(date +%F).tar.gz -C "$VOL" .
sudo docker compose startTo restore on a new server, run sudo docker compose up -d there once first, so the volume exists, then:
cd /opt/uptime-kuma
VOL=$(sudo docker volume inspect -f '{{ .Mountpoint }}' uptime-kuma-data)
sudo docker compose down
sudo find "$VOL" -mindepth 1 -delete
sudo tar -xzf /root/uptime-kuma-YYYY-MM-DD.tar.gz -C "$VOL"
sudo docker compose up -dA backup on the same disk does not survive the server, so copy the archive elsewhere, for example with scp, and end the job with a Push monitor call so a missed night raises an alert. The cron expression generator builds the schedule line for a nightly run.
Upgrades, and moving from 1.x to 2
cd /opt/uptime-kuma
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -fup -d recreates the container only when the image or the file changed; the volume stays. With a pinned tag, edit the version in compose.yaml first. Take a backup before every upgrade.
Without Docker, version 2 needs Node.js 20.4 or later; Node.js 14, 16 and 18 are no longer supported.
Why the monitor belongs in another location
A monitor on the server it watches cannot report that server down: when the machine, its network or its data centre fails, the alert fails with it. The same applies one level up, to a monitor in the same rack or network. Put Uptime Kuma in a different location from your production servers. VMHeaven runs servers in Amsterdam and Frankfurt, so a monitor in one city can watch servers in the other; the Frankfurt and Amsterdam comparison covers how the two differ. Standard KVM is orderable in Amsterdam; in Frankfurt the lines you can order at the moment are DMCA Ignored KVM and Pentesting KVM, and VPS in Germany shows what is in stock there.
- Checks from more places: the Globalping monitor type runs a ping, HTTP or DNS check from a community probe in a continent, country, city or network you name. Without an account Globalping allows 250 tests an hour, with an API token 500; a monitor that runs every 60 seconds uses 60 of them.
- Watch the watcher: nothing alerts you when Uptime Kuma itself stops. Run a second small instance in the other location, and let each one monitor the other’s status page.
When everything runs, check the setup from outside: the open port checker must not find port 3001 open (closed or no answer are both fine) and must find 443 open, and the SSL checker should show a valid certificate for your status domain.
Frequently asked
Is louislam/uptime-kuma:latest the current version?
No. The latest and 1 tags still point to the old 1.x line, which the project marks as deprecated. Use louislam/uptime-kuma:2, 2-slim if you need neither the embedded MariaDB nor Chromium, or a pinned tag such as 2.5.6. Moving an existing 1.x install to 2 converts the database on the first start, so back up the data directory first and do not interrupt it.
Can Uptime Kuma run on the same server it monitors?
It runs, but it cannot warn you when that server, its network or its data centre goes down, because it goes down at the same time. Put it on a separate small VPS, ideally in another location, and let a second instance or another check watch Uptime Kuma itself.
How do I reset the Uptime Kuma admin password?
On the server, run 'sudo docker compose exec uptime-kuma npm run reset-password' in the directory that holds compose.yaml. The tool finds the admin user and asks for the new password twice. Without Docker, run 'npm run reset-password' in the Uptime Kuma directory.