Self-host Uptime Kuma for status pages and alerts
Follow this on your own server. Start free, no card needed
You want to know when your site, API, or server goes down, and you want a status page you can point customers to without paying a monthly fee for something a single container can do. Uptime Kuma is an open-source monitoring tool that does both: it pings your endpoints on a schedule, sends you an alert the moment one fails, and gives you a public status page to show the result. By the end of this, you’ll have it running on your own server, reachable over HTTPS on your own domain, with its data actually surviving a restart.
What you’ll need
- A small server. Uptime Kuma is a single Node.js process with a SQLite database, so it doesn’t need much, but giving it at least 1 GB of RAM avoids the out-of-memory problems described in the troubleshooting section below. A DigitalOcean Basic Droplet with 1 GB RAM, 1 vCPU, and 25 GB SSD is $6/month as of today (checked against DigitalOcean’s own pricing page). Hetzner’s cost-optimized line is a reasonable alternative if you’d rather compare.
- A domain you control, with the ability to add an A record.
- Docker and Docker Compose installed on the server (Step 2 covers this).
- A few minutes on first visit to create the admin account. Uptime Kuma ships with no default login: whoever gets to the setup page first becomes the admin, so don’t leave it open to the internet before that step is done.
Step 1: point your domain at the server
Create the server first, note its public IP address, then add an A record for the subdomain you want to use, for example status.yourdomain.com, pointing at that IP. DNS propagation is usually fast but can take up to a few hours depending on your registrar’s TTL settings.
You can check it resolved with:
dig +short status.yourdomain.com
You should see the server’s IP address printed back. If nothing comes back yet, wait a bit before moving on. Doing this step first means by the time you get to HTTPS later, DNS has had time to settle.
Step 2: install Docker
Uptime Kuma’s own install docs recommend running it as a container, and that’s the setup this post uses throughout. SSH into your server and install Docker with the official convenience script:
curl -fsSL https://get.docker.com | sh
Docker Compose ships as a plugin with current Docker installs, so a separate install step usually isn’t needed. Confirm both are present:
docker --version
docker compose version
You should see a version number printed for each. If docker compose version fails, your Docker install is old enough that Compose isn’t bundled, and you’ll need to install the docker-compose-plugin package separately.
Step 3: write the Compose file and start Uptime Kuma
Create a directory for the app and a compose.yml inside it:
mkdir -p ~/uptime-kuma && cd ~/uptime-kuma
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- uptime-kuma-data:/app/data
volumes:
uptime-kuma-data:
The 1 tag tracks the current stable release line and updates with patch releases when you pull again, rather than pinning one exact build. The important part is the uptime-kuma-data named volume mounted at /app/data: that’s where Uptime Kuma keeps its SQLite database, and it’s what makes your monitors, notification settings, and status pages survive a container restart or rebuild. Skip the volume and a docker compose down or a server reboot that recreates the container will wipe every monitor you set up.
Start it:
docker compose up -d
Check it’s running:
docker compose ps
You should see the uptime-kuma service listed as running or healthy. At this point it’s only reachable on port 3001, which is fine, since Step 5 puts a reverse proxy in front of it.
Step 4: create the admin account and add your first monitor
Before DNS and HTTPS are in place, you can still reach the app directly over its port to get the admin account created, which closes the open-setup-page window as early as possible:
http://<server-ip>:3001
The first screen asks you to create a username and password. There’s no default login to change, just a blank slate. Once you’re in, add a monitor from the dashboard: give it a name, a type (HTTP(s), TCP port, ping, DNS, and others are supported), and the target to check. Set the check interval (60 seconds is a reasonable default for most sites) and save it.
While you’re there, go to Settings → Notifications and add at least one notification channel (email, Slack, Discord, a generic webhook, and dozens of others are supported). Use the Test button on the notification before relying on it: a misconfigured webhook URL or an email provider that silently rejects the message is the single most common way people discover, during a real outage, that their alerts were never actually being delivered.
You should see your new monitor’s status turn green (or red, if you pointed it at something unreachable) within one check interval.
Step 5: put Caddy in front of it for HTTPS
Running a monitoring dashboard on a bare HTTP port, with no certificate and no domain, is fine for testing but not for anything you’d actually rely on. Caddy is a reverse proxy that requests and renews a Let’s Encrypt certificate automatically, with no separate certbot setup.
Install Caddy directly on the server (outside the Compose file, so it can bind to ports 80 and 443 cleanly):
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Replace /etc/caddy/Caddyfile with:
status.yourdomain.com {
reverse_proxy localhost:3001
}
Reload Caddy to pick up the change:
sudo systemctl reload caddy
Caddy requests the certificate the first time it sees a request for that hostname, which only succeeds if the A record from Step 1 is already resolving to this server. You should see https://status.yourdomain.com load Uptime Kuma with a valid padlock, and plain http:// requests to the same hostname should redirect to HTTPS automatically. Once this works, close off direct access to port 3001 at your firewall so the app is only reachable through Caddy.
Step 6: back up the data volume
Everything that matters (monitors, notification settings, status page configuration, history) lives in the SQLite file inside the uptime-kuma-data volume. Back it up on a schedule rather than finding out you need it during an actual incident.
A simple cron-friendly approach, run from the host:
docker run --rm \
-v uptime-kuma_uptime-kuma-data:/data \
-v ~/uptime-kuma-backups:/backup \
alpine tar czf /backup/uptime-kuma-$(date +%F).tar.gz -C /data .
Add that as a daily cron job (crontab -e) and keep the last week or two of archives. You should see a new .tar.gz file land in ~/uptime-kuma-backups after it runs. To restore, stop the container, extract the archive back into the same volume path, and start it again.
Step 7: upgrading later
Because the Compose file pins the rolling 1 tag rather than an exact patch version, pulling again picks up the latest patch release on that line:
docker compose pull
docker compose up -d
Docker recreates the container with the new image while leaving the named volume untouched, so your data isn’t affected. Check the release notes on Uptime Kuma’s GitHub releases page before a major version jump specifically, since those can include breaking changes that a patch release wouldn’t.
The shortcut: do it with Railyard
Everything above is the complete manual path and it’s a reasonable way to run this if you want to own every piece of it. If you’d rather skip the server provisioning, Docker install, Compose file, and reverse-proxy config and get the same running app, Railyard’s Uptime Kuma template deploys the same image with the data volume already mounted at /app/data and HTTPS already handled. There’s nothing to configure after it deploys: no default login either way, you create the admin account on your first visit, same as the manual path. It’s the same app, same setup step, just without the server management in between.
If you’re new to Railyard generally, the quickstart and deploying with a one-click button cover the basics, and hosting costs breaks down what a small app like this typically runs across providers.
Troubleshooting
“Bind for 0.0.0.0:3001 failed: port is already allocated”. Something else on the server is already using port 3001, often a previous attempt at running the container that didn’t get removed. Check with docker ps -a and remove the stale container, or change the host side of the port mapping in compose.yml (for example "3002:3001") and update the Caddy reverse proxy target to match.
Caddy won’t issue a certificate. This is almost always DNS, not Caddy. If the A record from Step 1 hasn’t propagated yet, or points at the wrong IP, Let’s Encrypt’s validation request never reaches your server and the certificate request fails silently in the Caddy logs (sudo journalctl -u caddy -n 50). Re-run the dig command from Step 1 to confirm the record resolves to the right IP before troubleshooting anything else.
Monitors disappeared after a restart or redeploy. This means the container was running without the named volume, or with the volume mounted to the wrong path. /app/data is the only path Uptime Kuma writes its database to. If you ran a quick docker run without -v to test something, that container’s data is gone the moment it’s removed. Double-check docker volume ls shows uptime-kuma_uptime-kuma-data and that it’s the one mounted in your compose file.
Notifications aren’t actually arriving. Uptime Kuma logs a failed notification send in its own event log, but it’s easy to miss if you’re not looking for it. Always use the Test button in Settings → Notifications right after adding a channel, not just when you first set up a monitor. Webhook URLs that are slightly wrong, email providers that require an app password instead of your normal one, and Slack tokens that expired are the most common causes.
The container keeps restarting on a very small server. On a 512 MB droplet, Uptime Kuma’s Node.js process can get OOM-killed under load, especially if you add many monitors with short check intervals or enable monitors that use a headless browser. dmesg | grep -i "out of memory" will confirm it. The fix is more RAM, not more tuning: a 1 GB plan is the realistic minimum for comfortable, hands-off operation.
FAQ
Is Uptime Kuma free? Yes, it’s open source (MIT licensed) and self-hosted, so there’s no subscription. You pay only for the server it runs on.
Do I need a database? No. Uptime Kuma uses an embedded SQLite database inside its own data volume, so there’s nothing extra to provision or connect.
Can I monitor more than one site or server from a single instance? Yes, a single Uptime Kuma instance can run any number of monitors, each checked independently on its own schedule and type.
Is the status page public by default? Status pages are something you create and publish deliberately from the dashboard. Individual monitors aren’t exposed until you add them to a status page you choose to make public.
How is this different from UptimeRobot or Pingdom? Those are hosted services you pay monthly for and don’t control the infrastructure of. Uptime Kuma gives you the same core feature set (HTTP/TCP/ping checks, alerting, public status pages) running on a server you own, with your monitoring history staying in your own database rather than a vendor’s.
Once it’s running and backed up, this is one of the lowest-maintenance things you can self-host: it checks in on everything else so you don’t have to, and it only asks something of you when something’s actually wrong.
Why people pick Railyard for this
- Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
- One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
- One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
- Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
- Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.