Skip to main content

Uptime Kuma on GKE Autopilot — Lab Guide

📖 Configuration Guide

Overview

Estimated time: 45–90 minutes

Uptime Kuma is a self-hosted uptime monitoring tool for websites, APIs, TCP ports, and DNS records, with status pages and 90+ notification channels. This lab takes you through the full operational lifecycle of the Uptime Kuma on GKE Autopilot module on Google Cloud: deploy it, access and verify it, run it day-to-day, observe it, diagnose common problems, and tear it down.

The lab focuses on operating the GKE module and the Google Cloud platform, not on Uptime Kuma product features. For the complete list of provisioned services and every configuration input (organised by group), see the Configuration Guide — this lab deliberately does not duplicate that detail so it stays accurate over time.

Objectives

By the end of this lab you will be able to:

  • Deploy the module from the RAD platform and locate the resources it provisions.
  • Connect to the GKE cluster, find the Uptime Kuma workload, and verify it is healthy.
  • Explain why this module has no Cloud SQL instance and no Secret Manager entries, and why all durable state lives on a single Filestore (NFS) share.
  • Perform day-2 operations — inspect the workload, understand why it must stay at a single replica, update the version, and check the NFS-backed data.
  • Observe the workload with Cloud Logging and Cloud Monitoring.
  • Diagnose and resolve the most common deployment and runtime issues, including the SQLite-over-NFS journal-mode fix this module ships by default.
  • Tear the deployment down cleanly.

Prerequisites

  • Services_GCP (provides the VPC, GKE Autopilot cluster, Filestore/NFS networking, Artifact Registry, and shared service accounts this module depends on). You do not need to deploy this yourself first — the platform automatically detects whether it already exists in the target project and provisions it before this module if not (see Task 1).
  • A Google Cloud project with billing enabled.
  • gcloud CLI and kubectl installed; gcloud auth login and gcloud auth application-default login completed.
  • Project Owner (or equivalent) IAM on the project.
  • RAD platform access with permission to deploy modules into the project.

Set these shell variables once; every task below reuses them:

export PROJECT="<your-gcp-project-id>"
export REGION="us-central1" # the region you deploy into

Task 1 — Deploy the module [Automated]

  1. Click Deploy in the RAD platform top navigation, open UptimeKuma (GKE) from the Platform Modules list to start configuration, set project_id, and review the inputs. Configure only what you need — the Configuration Guide documents every input by group, with defaults. Review the estimated cost (if credits are enabled) and click Deploy, which opens the deployment status page with real-time logs.

  2. The platform schedules the workload onto the GKE Autopilot cluster, provisions no Cloud SQL instance (database_type = "NONE"), creates no Secret Manager entries, and runs no database-initialisation job — Uptime Kuma stores everything in an embedded SQLite database that it creates itself on first boot. The platform mounts a Filestore (NFS) share at the fixed container path /app/data. Because container_image_source = "custom" (the default), a Cloud Build step first builds a thin image FROM louislam/uptime-kuma that patches the hardcoded SQLite journal_mode from WAL to DELETE — WAL's shared-memory locking is unsafe on the NFS-backed /app/data volume this app uses (see Task 3.5) — and the built image is then mirrored into Artifact Registry (enable_image_mirroring = true). Because there is no database to provision, this deploy is dominated by the Cloud Build step, Autopilot node scheduling, and the NFS mount rather than by Cloud SQL creation — expect it to complete noticeably faster than database-backed GKE modules, but not as fast as a truly prebuilt-image module.

  3. Connect to the cluster and discover the namespace with name-agnostic filters:

    CLUSTER=$(gcloud container clusters list --project="$PROJECT" --format="value(name)" --limit=1)
    gcloud container clusters get-credentials "$CLUSTER" --region="$REGION" --project="$PROJECT"

    NS=$(kubectl get ns -o name | grep uptimekuma | head -1 | cut -d/ -f2)
    echo "Cluster: $CLUSTER Namespace: $NS"
    kubectl get all -n "$NS"

Task 2 — Access & verify [Manual]

  1. Confirm the workload is running and find its external address:

    kubectl get pods,svc -n "$NS"
    EXTERNAL_IP=$(kubectl get svc -n "$NS" \
    -o jsonpath='{.items[?(@.spec.type=="LoadBalancer")].status.loadBalancer.ingress[0].ip}')
    echo "External IP: $EXTERNAL_IP"
  2. Confirm the service is healthy. Both the startup and liveness probes are a plain HTTP GET / on port 3001 — Uptime Kuma's native port:

    curl -s -o /dev/null -w "%{http_code}\n" "http://${EXTERNAL_IP}/"   # expect 200

    The startup probe is deliberately generous (30 s initial delay, 10 s period, 30 failure threshold — up to several minutes), which covers first-boot SQLite schema creation on a fresh NFS volume.

  3. Open http://${EXTERNAL_IP} in a browser. On first access Uptime Kuma serves its setup wizard — create the admin account immediately. There is no default or auto-generated credential in Secret Manager to retrieve:

    gcloud secrets list --project="$PROJECT" --filter="name~uptimekuma"   # expect empty

    Until the admin account exists, the setup page is reachable by anyone who finds the external IP — do not leave a freshly deployed instance unconfigured for long.

  4. Add a first monitor (e.g. an HTTPS check against a site you own) and watch it turn green.


Task 3 — Operate & keep it running (Day-2) [Manual]

  1. Inspect the workload — deployment, pods, and the NFS-backed PVC:

    kubectl get deploy,pods,pvc -n "$NS"
    kubectl describe deploy -n "$NS"
  2. Do not scale beyond one replica. Both min_instance_count and max_instance_count default to 1 and should stay there — Uptime Kuma's embedded SQLite database is single-writer, and every pod would otherwise share the same NFS-mounted database file. Unlike modules that scale via a Redis-backed queue, there is no supported way to run more than one Uptime Kuma replica against the same data volume.

  3. Update the application version by changing the version input in the RAD platform and applying it via Update. Because this module is NFS-backed, the Foundation sets the Deployment's update strategy to Recreate rather than the default RollingUpdate — the running pod is fully terminated before the replacement starts, rather than surging a second pod that would otherwise write the same SQLite file concurrently. Expect a short gap in monitoring coverage during the update, not a seamless rolling replace.

  4. Confirm there are no secrets or init jobs to manage — this module creates neither:

    kubectl get secrets -n "$NS"
    gcloud secrets list --project="$PROJECT" --filter="name~uptimekuma" # expect empty
    kubectl get jobs -n "$NS" # expect none by default
  5. Verify the persistent state lives on Filestore (NFS), not ephemeral disk, and back it up:

    gcloud filestore instances list --project="$PROJECT"
    kubectl exec -n "$NS" deploy/"$(kubectl get deploy -n "$NS" -o jsonpath='{.items[0].metadata.name}')" -- ls -la /app/data

    Export a backup from the Uptime Kuma UI (Settings → Backup) periodically, in addition to any Filestore snapshot policy.

    SQLite over NFS — fixed by the module's custom build. Uptime Kuma unconditionally sets PRAGMA journal_mode = WAL on every boot (hardcoded in server/database.js, not configurable via env var). WAL relies on shared-memory byte-range locking between the DB file and its -wal sidecar, which NFS does not reliably provide — this combination previously produced observed SQLITE_CORRUPT errors even when the Filestore/NFS mount itself was correctly configured. This module now defaults container_image_source = "custom" (see Task 1.2), which builds a thin image FROM louislam/uptime-kuma that source-patches the PRAGMA to DELETE mode — DELETE only needs standard whole-file locking, which NFS handles correctly. Do not override container_image_source to "prebuilt": that skips the patch and re-exposes the WAL/NFS corruption risk. The UI export/backup step above is still good practice for disaster recovery, but is no longer compensating for a known corruption risk under normal operation.


Task 4 — Observe: Logging & Monitoring [Manual]

  1. Logs — from kubectl or the Logs Explorer:

    kubectl logs -n "$NS" deploy/"$(kubectl get deploy -n "$NS" -o jsonpath='{.items[0].metadata.name}')" --tail=50

    Logs Explorer filter: resource.type="k8s_container" AND resource.labels.namespace_name="<namespace>".

  2. Monitoring — open the GKE / Kubernetes dashboards and review pod CPU and memory utilisation and restart counts (a restarting pod is the fastest signal that monitoring coverage has gaps). The module can optionally provision a Cloud Monitoring uptime check against Uptime Kuma's own GET / endpoint (uptime_check_config.enabled = false by default) — an outside-in signal that your monitoring system is itself up. This is separate from the monitors you configure inside the Uptime Kuma UI.


Task 5 — Troubleshoot & debug [Manual]

Durable techniques for the failure modes you are most likely to hit. These are platform-level diagnostics and do not change with Uptime Kuma releases.

  • Pod not Ready / CrashLoopBackOff: inspect events and logs. The liveness probe is HTTP GET / on port 3001; a workload that never opens that port will fail Ready.
    kubectl describe pod -n "$NS" <pod>          # Events section shows scheduling/probe/mount errors
    kubectl logs -n "$NS" <pod> --previous # logs from the crashed container
  • Pending pod / NFS not mounting: confirm the Filestore instance is READY and the PVC is Bound before assuming an application bug:
    gcloud filestore instances list --project="$PROJECT"
    kubectl get pvc -n "$NS"
    kubectl describe pod -n "$NS" <pod> # look for mount errors in Events
  • "database is corrupted" / SQLITE_CORRUPT in logs: see the Task 3.5 note above. This module's custom build already patches SQLite's journal mode from WAL to DELETE, so first confirm container_image_source = "custom" was actually applied — check gcloud builds list --project="$PROJECT" for the build and kubectl exec -n "$NS" deploy/<name> -- grep journal_mode /app/server/database.js to confirm the deployed image has DELETE, not WAL. If it shows "prebuilt" in deploy.tfvars or the grep still shows WAL, that is the root cause — fix container_image_source and redeploy. If the patch is confirmed applied and corruption still occurs, restore from the most recent Uptime Kuma backup, or delete the SQLite file under /app/data and let Uptime Kuma recreate its schema on next boot (monitor history is lost back to the last backup).
  • Monitors show gaps in history: confirm the pod has not been restarting (kubectl get pods -n "$NS" restart count) and that only one replica is running — max_instance_count above 1 risks lock contention on the shared SQLite file, which can itself present as gaps or corruption.
  • No external IP: check kubectl describe pod events for resource or quota issues, and confirm the LoadBalancer Service has an assigned IP (kubectl get svc -n "$NS").
  • Image pull errors / stale image: this module builds a custom image via Cloud Build (container_image_source = "custom", the default) and pushes it to Artifact Registry — confirm the build succeeded (gcloud builds list --project="$PROJECT"), the image exists in Artifact Registry, and the node service account can pull it. If the build never runs, check deploy.tfvars for a container_image_source override to "prebuilt" — that silently skips the build and deploys the unpatched upstream image (see Task 3.5).
  • Update seems to cause downtime: expected — see the Recreate strategy note in Task 3.3. This is a deliberate trade-off to avoid two pods writing the SQLite file at once, not a stuck rollout.

See the Configuration Guide's Configuration Pitfalls section for setting-specific gotchas (including why enable_nfs, nfs_mount_path, and max_instance_count are marked Critical/High risk).


Task 6 — Tear down [Automated]

On the Deployments page, open the deployment and click the Trash icon (Delete). Delete runs terraform destroy and is irreversible (the deployment record is retained for history). If a deployment is stuck and the RAD platform can no longer manage it (for example after manual changes that conflict with the Terraform state), use Purge instead — it removes the deployment from RAD's records without destroying the cloud resources (it makes RAD forget the project). This removes everything the module created — the Kubernetes workload and namespace, and the Filestore NFS share (including the SQLite database with all monitors and history — export a backup first if you want to keep them). There is no Cloud SQL instance, Secret Manager secret, or GCS bucket to clean up by default. Resources owned by Services_GCP (the VPC, GKE cluster, shared Filestore networking, registry) are managed separately and are not removed here.


Summary

TaskTypeOutcome
1 — DeployAutomatedModule deploys the GKE workload with a Filestore NFS mount at /app/data and builds/mirrors a custom image patched for NFS-safe SQLite — no Cloud SQL, secrets, or init jobs
2 — Access & verifyManualConnect to the cluster; health check passes; create the initial admin account on the first-run setup page
3 — OperateManualInspect workload, keep replica count at 1, update version (Recreate strategy), verify and back up NFS-backed SQLite state
4 — ObserveManualQuery Cloud Logging; review pod restarts and CPU/memory; optionally monitor the monitor
5 — TroubleshootManualDiagnose pod, NFS-mount, SQLite-over-NFS corruption, scheduling, and image-pull issues
6 — Tear downAutomatedDelete (Trash) removes all module resources