Skip to main content

Trilium on GKE Autopilot

Trilium on GKE Autopilot

Trilium Notes (the actively maintained TriliumNext fork — not the archived zadam/trilium) is an open-source, hierarchical, self-hosted note-taking application with an embedded SQLite database. This module deploys Trilium on GKE Autopilot on top of the App_GKE foundation, which provisions and manages the shared Google Cloud and Kubernetes infrastructure.

This guide focuses on the cloud services Trilium uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics that are common to every GKE application — Workload Identity, ingress, autoscaling, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC Service Controls, backups, and the deployment lifecycle — refer to the App_GKE foundation guide rather than repeating them here.


1. Overview

Trilium runs as a single Node.js/Express pod on GKE Autopilot. The deployment wires together a deliberately small set of Google Cloud services:

CapabilityGoogle Cloud serviceNotes
ComputeGKE AutopilotNode.js pod, 1 vCPU / 1 GiB by default — but see scaling notes below
DatabaseNone (embedded SQLite)Trilium's entire document store is a single SQLite file, document.db, on the persistent volume
Object storageCloud Storage (default) or block PVCGCS FUSE volume, or a StatefulSet PVC for larger note collections
SecretsSecret ManagerNone generated — Trilium has no env-var-driven credential
IngressCloud Load BalancingExternal LoadBalancer by default (Trilium is a browser-facing web UI)

Sensible defaults worth knowing up front:

  • No database engine to manage. database_type = "NONE" — there is no Cloud SQL instance, no connection string, and nothing to back up separately from the data volume.
  • Single-replica only. min_instance_count = max_instance_count = 1. Trilium's embedded SQLite database has no multi-writer support — running more than one pod risks database corruption from concurrent writes.
  • service_type = "LoadBalancer" by default. Trilium is a browser-facing web UI, so it is exposed externally out of the box (unlike database-style workloads, which default to ClusterIP).
  • No seeded credential. Trilium has no env-var-driven auth bootstrap. On first visit, the app itself presents a "Set Password" screen; complete it before sharing the URL.
  • Health probe is /api/health-check, not /. The root path (/) returns a 302 redirect to the setup/login screen. Only /api/health-check returns an unauthenticated 200 {"status":"ok"} — confirmed live via local container testing.
  • Block PVC recommended for larger note collections. Set stateful_pvc_enabled = true to avoid GCS FUSE I/O overhead/locking quirks on the embedded SQLite file. The stateful_pvc_storage_class default is "standard" (HDD pd-standard) — Trilium needs no SSD IOPS, and HDD draws from the much larger DISKS_TOTAL_GB quota instead of the tight SSD_TOTAL_GB quota.
  • fsGroup/mount_options set to 1000. Trilium's container runs as the node user, uid/gid 1000 (confirmed via docker run ... id node); without matching ownership, the volume mounts root-owned and the app fails to boot.

2. Google Cloud Services & How to Explore Them

All commands assume you have run gcloud container clusters get-credentials <cluster> --region <region> --project <project> and that PROJECT, REGION, and NAMESPACE are set. The namespace and other identifiers are reported in the deployment Outputs.

A. GKE Autopilot — the Trilium workload

Trilium runs as a single pod (Deployment by default, or a StatefulSet when stateful_pvc_enabled = true). Because it must stay at exactly one replica, there is no meaningful horizontal autoscaling to observe.

  • Console: Kubernetes Engine → Workloads → select the Trilium workload for pods, revisions, and events. Kubernetes Engine → Services & Ingress shows the external IP.
  • CLI:
    kubectl get pods,svc -n "$NAMESPACE"
    kubectl logs -n "$NAMESPACE" deploy/<service-name> --tail=100

See App_GKE for Autopilot scaling and the workload type (Deployment vs StatefulSet).

B. Cloud Storage / block PVC — the Trilium data directory

The entire application state (SQLite document.db, attachments, revision history, settings) lives under /home/node/trilium-data, mounted either via GCS FUSE (default) or a StatefulSet block PVC (stateful_pvc_enabled = true, recommended for larger note collections).

  • Console: Cloud Storage → Buckets (GCS FUSE mode); Kubernetes Engine → Storage (PVC mode).
  • CLI:
    gcloud storage buckets list --project "$PROJECT"          # GCS FUSE mode
    kubectl get pvc -n "$NAMESPACE" # PVC mode

See App_GKE for CMEK options and GCS Fuse mounts.

C. Networking & ingress

By default the workload is exposed through an external Cloud Load Balancing IP. A custom domain with a Google-managed certificate can be enabled, and a static IP can be reserved so the address survives redeploys.

  • Console: Network services → Load balancing; VPC network → IP addresses.
  • CLI:
    kubectl get ingress,svc -n "$NAMESPACE"
    gcloud compute addresses list --project "$PROJECT"

See App_GKE for custom domains and static IP details.

D. Cloud Logging & Monitoring

Pod stdout/stderr flow to Cloud Logging; GKE metrics flow to Cloud Monitoring. Optional uptime checks and alert policies are available.

  • Console: Logging → Logs Explorer; Monitoring → Dashboards / Alerting.
  • CLI:
    gcloud logging read 'resource.type="k8s_container" AND resource.labels.namespace_name="'"$NAMESPACE"'"' \
    --project "$PROJECT" --limit 50

3. Trilium Application Behaviour

  • No first-deploy database setup job. Trilium creates and migrates its own SQLite schema on first web visit, via its own setup wizard — there is no Terraform-managed db-init job to inspect.
  • First-run "Set Password" screen. Navigating to the root URL for the first time presents a password-setup form (no default admin/username — Trilium is a single-user app). There is no pre-seeded credential in Secret Manager to look up.
  • Health path. Startup and liveness probes target /api/health-check, which returns 200 {"status":"ok"} once the HTTP server is listening.
  • Single-writer constraint. Never raise max_instance_count above 1 — the embedded SQLite database is not safe for concurrent writers from multiple pods.
  • PVC vs GCS FUSE trade-off. GCS FUSE (default) is simplest and needs no extra quota planning; a StatefulSet block PVC gives real POSIX file locking and lower I/O overhead for larger collections, at the cost of consuming regional disk quota (mitigated here by defaulting to HDD, not SSD).

4. Configuration Variables

Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for Trilium are listed; every other input is inherited from App_GKE with its standard behaviour and defaults.

Group 3 — Application Identity

VariableDefaultDescription
application_nametriliumBase name for resources. Do not change after first deploy.
application_versionlatestDocker image version tag; mapped to a pinned build ARG (TRILIUM_VERSION) internally.
enable_passwordfalseReserved for parity with other single-user editor modules. No effect — Trilium has no env-var-driven password bootstrap.

Group 4 — Runtime & Scaling

VariableDefaultDescription
deploy_applicationtrueSet false to provision infrastructure only.
cpu_limit1000mCPU per pod.
memory_limit1GiMemory per pod; Trilium is lightweight, raise only for very large note collections.
min_instance_count / max_instance_count1 / 1Keep both at 1 — no multi-writer support on the embedded SQLite database.
enable_image_mirroringtrueMirror the Trilium image into Artifact Registry before deployment.

Group 6 — GKE Backend & Cluster

VariableDefaultDescription
service_typeLoadBalancerTrilium is a browser-facing web UI, exposed externally by default.
workload_typenullAuto-resolves to StatefulSet when stateful_pvc_enabled = true, otherwise Deployment.

Group 7 — StatefulSet

VariableDefaultDescription
stateful_pvc_enablednullRecommended true for larger note collections — real POSIX file locking on the SQLite document.db.
stateful_pvc_size20GiPer-pod PVC size.
stateful_pvc_mount_path/home/node/trilium-dataContainer mount path.
stateful_pvc_storage_classstandardHDD by default — no SSD IOPS need; keeps deployments off the tight SSD_TOTAL_GB quota.
stateful_fs_group1000Matches Trilium's uid/gid (the node user).

Group 16 — Database Backend

VariableDefaultDescription
database_typeNONENot referenced — Trilium has no SQL database (embedded SQLite only).

Group 10 — Observability & Health

VariableDefaultDescription
startup_probeHTTP /api/health-check, 15s delayStartup probe.
liveness_probeHTTP /api/health-check, 30s delayLiveness probe.
uptime_check_configdisabledOptional Cloud Monitoring uptime check on /api/health-check.

Group 19 — Custom Domain, Static IP & Networking

VariableDefaultDescription
reserve_static_iptrueStable external IP across redeploys.

5. Outputs

OutputDescription
service_nameKubernetes Service name.
namespaceNamespace the workload runs in.
service_external_ipExternal LoadBalancer IP (when a static IP is reserved).
service_urlURL to reach Trilium.
storage_bucketsCreated Cloud Storage buckets.
container_image / container_registryDeployed image and Artifact Registry repo.
deployment_id / tenant_id / resource_prefixNaming identifiers.
kubernetes_readyWhether the cluster/workload is ready.

6. Configuration Pitfalls & Sensible Defaults

Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).

SettingSensible valueRiskConsequence if wrong
max_instance_count1CriticalRaising it risks concurrent writers corrupting the embedded SQLite database.
stateful_fs_group / GCS mount_options1000CriticalWrong uid/gid mounts the data directory root-owned; the non-root Trilium process fails to boot.
First-visit "Set Password" stepComplete immediatelyCriticalAn un-set-password Trilium instance on a public LoadBalancer IP is reachable by anyone until the password is set.
startup_probe / liveness_probe path/api/health-checkHighPointing probes at / gets a 302 redirect, which most HTTP health checks treat as a failure, blocking the pod from ever becoming Ready.
stateful_pvc_storage_classstandard (HDD)Mediumstandard-rwo (SSD) draws from the tight SSD_TOTAL_GB quota unnecessarily for a workload with no IOPS need.
service_typeLoadBalancer for normal useMediumSetting ClusterIP makes the note-taking UI unreachable from a browser without a port-forward.

For the foundation behaviour referenced throughout — IAM and Workload Identity, autoscaling, ingress and certificates, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, backups, and image mirroring — see App_GKE. Trilium-specific application configuration shared with the Cloud Run variant is described in Trilium_Common.