Skip to content

How to Deploy Checkmate on a VPS

Updated Aug 2026

verified on Ubuntu 26.04 · Aug 2026

Self-host Checkmate on a VPS — app container plus MongoDB, behind HTTPS — including the pinned MongoDB tag that refuses to boot on a current Ubuntu kernel, and how to fix it.

Before you start
  • A VPS with at least 2 GB RAM — MongoDB is the reason, not the app
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server — the app needs to know its own URL
  • Docker Engine + Compose installed (see the base guide below)

What Checkmate is

Checkmate is a self-hosted uptime and incident monitor with an unusually polished dashboard. It checks endpoints over HTTP, ping, port and SSL, watches Docker containers and game servers, runs PageSpeed reports, tracks incidents, and publishes status pages. Under the hood it's a TypeScript/Node application backed by MongoDB.

Two things distinguish it from the simpler monitors on this site. The first is that it is two services, not one — the app image and a MongoDB you have to run and back up. The second is that server hardware metrics are not built in: CPU, RAM and disk for your machines come from a separate agent called Capture, installed per machine, and you don't need it at all if you only want uptime checks.

If you want the lightest possible uptime monitor, Uptime Kuma is one container with embedded storage and a faster start. Checkmate is the pick when you want PageSpeed auditing, incident tracking and a nicer front end, and you don't mind running a database.

Before you start: the MongoDB kernel problem

This is the one that will stop your deploy dead, so it comes before the install rather than in troubleshooting.

The reference Compose file in Checkmate's repository pins MongoDB to the mongo:8.0 tag. That specific tag has a known incompatibility with Linux kernel 6.19 and newer: the container refuses to start and logs

MongoDB cannot start: Linux kernel versions 6.19 and newer has a known
incompatibility

tracked upstream as SERVER-121912. A current Ubuntu LTS ships a kernel new enough to hit this, so on a freshly provisioned box the reference file fails outright — the mongodb service never becomes healthy and the checkmate service, which depends on it, never starts either. Nothing about the error message points at Checkmate, which is what makes it a bad half hour.

The fix is a one-character change: use the mongo:8 tag, which resolves to a patched release. The install below does exactly that, with sed, before the stack is ever brought up.

Check what kernel you're on if you're curious whether it applies to you:

uname -r

Either way, rewriting the tag is harmless — do it and move on.

Server sizing

Checkmate itself is a modest Node application. MongoDB is what sets the floor.

  • 2 GB RAM / 1–2 vCPU — the sensible starting point for a personal or small-team install.
  • 4 GB RAM — comfortable, and what to choose if you're running many checks, keeping longer history, or running PageSpeed reports frequently.
  • 1 GB RAM — don't. MongoDB and Node together on a 1 GB box is how you get a monitor that gets OOM-killed and stops telling you about outages, which is the worst possible failure for this class of tool.

Disk: 20–40 GB. Check history and PageSpeed results accumulate, and MongoDB is not the most compact storage engine you'll ever run.

Put it on its own box. A monitor must not run on the machine it monitors — if the app server dies, so does the monitor living on it, and you hear about the outage from a customer. A Hetzner CX22 (2 vCPU / 4 GB), a Kamatera instance sized to taste, or a 2 GB DigitalOcean droplet all work.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root deploy user and a ufw firewall. If not, work through Docker & Compose on Ubuntu first.

Open SSH and the reverse proxy ports. Checkmate's own port and MongoDB's stay internal:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status verbose

Port 52345 must never be open to the internet. That's the app's own port and it serves plain HTTP — no TLS of its own, at all. The reverse proxy is the only thing that should ever reach it. MongoDB should never be reachable from outside either; in the reference stack it only listens on the Compose network, which is exactly where it belongs.

Outbound access matters too, since the whole job is reaching the things you monitor. If your provider filters egress, allow ICMP before you configure ping checks.

Install Checkmate

Fetch the reference Compose file, fix the MongoDB tag, then bring it up with a generated secret:

mkdir ~/checkmate && cd ~/checkmate
curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.yaml
sed -i 's|image: mongo:8.0|image: mongo:8|' docker-compose.yaml

That single sed is the kernel fix from the section above. Confirm it landed before you go further:

grep 'image: mongo' docker-compose.yaml

You should see image: mongo:8. Now start the stack:

# put the secret in .env so every later compose command sees it too — the inline
# form (VAR=... docker compose up) applies to that one command and nothing else
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env
docker compose up -d
docker compose ps
docker compose logs --tail 50

docker compose ps should show both services, with mongodb reporting healthy — the app has a dependency on that health check, so if Mongo isn't healthy the app won't start at all. Once it's up, the dashboard answers on http://SERVER_IP:52345.

Make the secret permanent

Passing JWT_SECRET inline on the command line works for the first boot and is a trap for every boot after it. JWT_SECRET signs authentication tokens: if it changes, every session is invalidated and everyone is logged out. Start it inline once and the next docker compose up -d without it either fails or comes up with a different secret.

Put it in a .env file next to the compose file, which Compose reads automatically:

cat > .env <<ENV
JWT_SECRET=$(openssl rand -hex 32)
CLIENT_HOST=https://status.example.com
ENV
chmod 600 .env

Then recreate the stack so both variables are in the environment for good:

docker compose up -d

Two variables, both worth understanding:

  • JWT_SECRET — required. Signs auth tokens. Generate it once, keep it secret, keep it stable, and back it up with everything else.
  • CLIENT_HOST — the public URL the application is reached at. It drives CORS and the absolute links in invitations, notifications and status pages. The default assumes localhost, so anything you reach at a domain needs this set or the front end will call an API origin that doesn't match and you'll get a dashboard that loads and then does nothing.

There's also DB_CONNECTION_STRING, which points at MongoDB. The reference file sets it to the bundled service; change it only if you're bringing your own MongoDB.

HTTPS + domain

Point an A record for status.example.com at the server's public IP and wait for it to resolve, then terminate TLS in front of 127.0.0.1:52345. The simplest path is Automatic HTTPS with Caddy:

status.example.com {
    reverse_proxy 127.0.0.1:52345
}

CLIENT_HOST must match this hostname exactly, scheme and all — https://status.example.com, not http://, not a bare domain, not a trailing slash. That coupling is the single most common cause of "the page loads but nothing works" on this app, and the fix is always to align the two and recreate the container.

If Caddy runs as a container rather than on the host, 127.0.0.1 is the proxy's own loopback. Put both in one compose file and proxy to the service name instead, dropping the host port publish.

While you're here, make sure the compose file publishes the app on loopback only. If the port line reads 52345:52345, change it to 127.0.0.1:52345:52345 — otherwise the plain-HTTP app is reachable directly on the public IP, bypassing your certificate entirely.

First login

Load https://status.example.com. On a fresh install you're redirected to the registration page, and the first account created becomes the superadmin. There is no default password shipped with the application.

Create your own account first, immediately. After that, additional people are added by invitation from the team settings — you enter an email address and pick a role — rather than by open sign-up, which is the behaviour you want on an internet-facing dashboard. Email invitations need working SMTP configured; if you haven't set that up yet, plan on the invite flow not delivering until you have.

Then do the usual hygiene: a strong unique password on the superadmin account, and a look through settings to confirm nothing is more open than you expect. This dashboard lists every hostname and endpoint you monitor — it's an infrastructure map.

Set up monitors

Configure notifications before monitors, so the first real failure actually reaches you. A monitor with no notification route is a dashboard.

Then add checks that test more than "the port answers":

  • HTTP/HTTPS uptime against a real health endpoint rather than the homepage.
  • Ping for raw host reachability, so you can tell "the box is gone" from "the app crashed".
  • Port checks for databases and anything without an HTTP surface.
  • SSL checks, which warn you before a certificate expires — the renewal that quietly stopped working is a classic outage.
  • Docker checks for container state on hosts you can reach.
  • PageSpeed reports on your public pages, which is the feature that pushes people towards Checkmate over lighter monitors. Run these on a slow schedule; they're much heavier than an uptime check.

Set intervals and retry counts that match reality. One failed check on a short interval is usually noise; several in a row is an incident. A monitor that cries wolf gets ignored.

Hardware metrics need Capture. If you want CPU, RAM and disk graphs for your servers, that comes from the separate Capture agent installed on each machine, not from Checkmate itself. It's optional — skip it entirely if uptime is all you need, or run a dedicated metrics tool like Beszel alongside for that half of the job.

Backups

Three things to preserve, and all three matter:

  1. The MongoDB data — your monitors, history and incidents.
  2. .env — the JWT_SECRET, without which restored sessions are dead.
  3. docker-compose.yaml — including your mongo:8 fix.

Dump the database with Mongo's own tool rather than copying files out from under a running server:

cd ~/checkmate
docker compose exec -T mongodb mongodump --archive --gzip \
  > checkmate-db-$(date +%F).archive.gz
tar czf checkmate-config-$(date +%F).tar.gz .env docker-compose.yaml

Restoring is the mirror image:

Substitute the archive you actually want to restore:

docker compose exec -T mongodb mongorestore --archive --gzip --drop \
  < checkmate-db-YYYY-MM-DD.archive.gz

Copy both files off the box and encrypt the config archive — it contains your JWT_SECRET. Automate the dump on a schedule, and test a restore at least once against a throwaway stack. And note the irony this class of tool always carries: the server that tells you other servers died needs its own recovery story.

Upgrades

cd ~/checkmate
docker compose pull
docker compose up -d

Back up first — the database schema can migrate on startup and migrations are one-way.

Two upgrade-specific cautions for this stack. First, if you re-download the reference compose file to pick up upstream changes, you will re-introduce the mongo:8.0 pin and your stack will stop booting on a current kernel. Re-run the sed, or keep your compose file under version control and merge changes in deliberately. Second, don't let a major MongoDB version jump happen by accident — check the release notes before changing that image tag beyond the patched line.

Troubleshooting

MongoDB won't start, and the app never starts either. Read the Mongo logs first: docker compose logs mongodb. If you see "Linux kernel versions 6.19 and newer has a known incompatibility", that's the pinned tag. Apply the sed above, then docker compose up -d to recreate the service. This also comes back after any re-download of the upstream compose file.

The app container restarts in a loop. Usually a missing JWT_SECRET, or MongoDB not being healthy yet. docker compose logs checkmate names which. Because the app depends on Mongo's health check, a Mongo problem always presents as both services being unavailable — always diagnose the database first.

The dashboard loads but nothing happens — no data, failing logins. CLIENT_HOST doesn't match the URL you're actually using. Set it to the exact public origin (https://status.example.com), recreate the container, and hard-refresh. Your browser's dev tools will show the front end calling the wrong origin and being blocked by CORS.

Everyone got logged out after a restart or a move. JWT_SECRET changed. That happens when it was only ever passed inline on the command line. Put it in .env, and restore that same value if you're rebuilding from a backup.

Certificate errors, or the site works on :52345 but not on the domain. DNS isn't resolving to the box yet, or the proxy isn't running. Confirm with dig status.example.com, check ports 80 and 443 are open in ufw and in any cloud firewall in front of it, and remember that the app's own port serves plain HTTP — if http://SERVER_IP:52345 works from outside, that port is published too broadly and should be bound to loopback.

Ping monitors all fail while HTTP monitors pass. ICMP is blocked, either by the provider's network or on the host. Allow it outbound, or use port checks instead.

Verification + next steps

You're done when you can: load https://status.example.com over a valid certificate, log in as the superadmin you created, confirm mongo:8 is the running image (docker compose ps), watch a monitor go green, then break something deliberately and receive the notification on a real device. Then restart the whole stack — docker compose down && docker compose up -d — and confirm you are still logged in and your history is intact. That last step is what proves JWT_SECRET and the database volume are both persistent.

From there: add SSL expiry checks to every certificate you own, put PageSpeed reports on your public pages, and decide whether you want Capture for hardware metrics or a dedicated tool like Beszel alongside. For the ranked host picks, see Best VPS for Monitoring & Uptime.

Next steps

How to self-host CheckmateBest VPS for Monitoring & UptimeAutomatic HTTPS with CaddyRun Claude Code with Ollama on Your Own VPSDeploy Coolify on a VPSHow to Deploy AnythingLLM on a VPSHow to Deploy authentik on a VPSHow to Deploy Beszel on a VPSHow to Deploy Bitwarden on a VPSHow to Deploy docker-mailserver on a VPSHow to Deploy Gatus on a VPSHow to Deploy Gitea on a VPSHow to Deploy Grafana on a VPSHow to Deploy Headscale on a VPSHow to Deploy Healthchecks on a VPSHow to Deploy Home Assistant on a VPSHow to Deploy Immich on a VPSHow to Deploy Jan on a VPSHow to Deploy LibreChat on a VPSHow to Deploy LocalAI on a VPSHow to Deploy Mailcow on a VPSHow to Deploy Mailu on a VPSHow to Deploy n8n on a VPSHow to Deploy NetBird on a VPSHow to Deploy Netdata on a VPSHow to Deploy Nextcloud on a VPSHow to Deploy Next.js to a VPSHow to Deploy Ollama on a VPSHow to Deploy Open WebUI on a VPSHow to Deploy OpenHands on a VPSHow to Deploy Pangolin on a VPSHow to Deploy Passbolt on a VPSHow to Deploy Plausible Analytics on a VPSHow to Deploy Psono on a VPSHow to Deploy Stalwart on a VPSHow to Deploy Supabase on a VPSHow to Deploy TeamPass on a VPSHow to Deploy Twenty CRM on a VPSHow to Deploy Uptime Kuma on a VPSHow to Deploy Vaultwarden on a VPSHow to Deploy wg-easy on a VPSHow to Deploy Zabbix on a VPSDocker & Compose on Ubuntu 26.04Building AI Workflows with n8nInstall Open WebUI with OllamaAdding AI-Powered Insights to Plausible AnalyticsBuilding AI-Powered Apps with Supabase and pgvector

Search SelfHost Atlas

Search apps, comparisons, guides, and categories.

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.