Skip to content

How to Deploy Checkmate on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Aug 2026
We earn commissions when you shop through the links below. Full disclosure →

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 the one safe way around 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)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

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.

Check what kernel you're on:

uname -r

MongoDB's own documentation puts the affected range at 6.19 through 7.0.13 and gives exactly one remedy: run kernel 7.0.14 or later. On Ubuntu 26.04 that is not currently available — the newest kernel the archive offers is still a 7.0.0 build — so on that OS you cannot upgrade your way out of it today.

That refusal is a guard, not a bug, and most of the workarounds circulating for it are worse than the problem. MongoDB 8.x vendors a build of TCMalloc that violates the kernel's rseq ABI; on an affected kernel it really can crash. Anything that makes 8.x start on such a kernel is removing the check, not fixing the allocator:

  • Setting GLIBC_TUNABLES=glibc.pthread.rseq=1 forces rseq on under the unfixed allocator.
  • Pinning an older 8.x build works only because the guard did not exist yet. We checked the images: 8.0.12 and 8.0.20 have no guard, 8.0.28 onward do, and 8.2.12 has none simply because the 8.2 line reached end of life before the guard was added.
  • mongo:8 and mongo:latest resolve to 8.3.9 as of this writing, which does carry the guard — so retagging to those does not start either. (An earlier version of this guide recommended mongo:8; that advice was wrong on both counts and has been replaced.)

What does work, safely, is MongoDB 7.0. It is still a supported release line, its release notes carry no such kernel warning, and its current build ships no guard because it does not vendor the affected allocator. We ran the full Checkmate stack against mongo:7 (7.0.41) on kernel 7.0.0-1011-gcp — both containers healthy, the app serving, and Connected to MongoDB in the server log. The install below pins that, with sed, before the stack is ever brought up.

One limit worth stating plainly: this is the right move for a fresh install. MongoDB will not open 8.0 data files with a 7.0 server, so if you already have a running 8.0 deployment on an unaffected kernel, leave it alone rather than retagging it downward.

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. Any VPS with 2 GB RAM or more (2 vCPU / 4 GB gives comfortable headroom) works.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

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:7|' 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:7. 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:7 pin.

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 MongoDB version jump happen by accident — a move from 7.0 up to 8.x is a one-way data migration, and on an affected kernel it also puts you straight back into the startup refusal.

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. Do not reach for GLIBC_TUNABLES or an older 8.x tag to silence it — see the section above for why those start the server on an allocator that can still crash.

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:7 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 Checkmate →More self-hosted monitoring tools →Best VPS for Monitoring & Uptime →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

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