Calibre-Web on GKE Autopilot
Calibre-Web is a clean, self-hosted web app for browsing, reading and downloading
ebooks from an existing Calibre library — it serves an in-browser reader, an OPDS
feed, user management and Kobo sync on top of the upstream LinuxServer.io
calibre-web image. This module deploys Calibre-Web on GKE Autopilot on top of
the App_GKE foundation, which provisions and manages the shared
Google Cloud and Kubernetes infrastructure; CalibreWeb_GKE is a thin wrapper that
supplies Calibre-Web's own configuration (image, ports, probes, storage wiring) and
forwards everything else straight through.
This guide focuses on the cloud services Calibre-Web 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
Calibre-Web runs as a single, stateful workload. It has no external database —
all of its state (the application database, the Calibre library metadata database,
configuration, cache, and logs) lives in internal SQLite files under /config. The
deployment wires together a focused set of Google Cloud services:
| Capability | Google Cloud service | Notes |
|---|---|---|
| Compute | GKE Autopilot | Calibre-Web pods on port 8083, 1 vCPU / 1 GiB by default |
| Workload type | Kubernetes StatefulSet | Auto-selected because stateful_pvc_enabled = true by default |
| Config/library persistence | GKE block Persistent Volume (standard-rwo, SSD-backed) | Mounted at /config; holds app.db, Calibre's metadata.db, config, cache, logs |
| Object storage | Cloud Storage | A storage-suffixed bucket is always provisioned, but is not mounted while the StatefulSet PVC is in use (see §2.C) |
| Secrets | Secret Manager | Auto-generated CALIBRE_ADMIN_PASSWORD — provisioned but not the credential Calibre-Web actually authenticates with on first login (see §3) |
| Database | None | database_type = "NONE"; no Cloud SQL instance, user, or db-init job |
| Ingress | Kubernetes Gateway API (when custom domain configured) or in-cluster ClusterIP | service_type defaults to ClusterIP, not LoadBalancer (see §2.E) |
Sensible defaults worth knowing up front:
- Stateful, PVC-backed by default.
stateful_pvc_enabled = trueandworkload_typeis leftnull, which auto-resolves toStatefulSet(per the App_GKE "StatefulSet auto-select" behaviour). A real block PVC is used instead of gcsfuse specifically because Calibre-Web's SQLite files (app.db,metadata.db) would be corrupted by GCS FUSE's relaxed consistency model. - The PVC storage class defaults to SSD (
standard-rwo). This draws from the regionalSSD_TOTAL_GBquota, which is small on quota-constrained projects. Calibre-Web doesn't need SSD IOPS for its SQLite workload — consider overridingstateful_pvc_storage_class = "standard"(HDDpd-standard) to preserve SSD quota for other apps while keeping the block-device write-locking integrity SQLite needs. - Single replica by default.
min_instance_count = 1,max_instance_count = 1. Because the StatefulSet usesvolumeClaimTemplates, each replica gets its own, independent PVC — scaling beyond 1 does not share/configacross pods; it silently forks the library and config into separate, unsynchronised copies per pod. Do not raisemax_instance_countwithout an external synchronisation strategy. - No database.
database_type = "NONE"; there is nodb-initjob and none of the database-related variables in this module are referenced. - No Redis.
enable_redisis declared for foundation-variable mirroring but is not forwarded —main.tfhardcodesenable_redis = falseto the Foundation call regardless of the variable's value. service_typedefaults toClusterIP, notLoadBalancer. Combined with the defaultapplication_domains = [], the deployment has no external access configured out of the box even thoughenable_custom_domain = trueby default — the Gateway is provisioned but has no hostname to route until you supplyapplication_domainsor switchservice_typetoLoadBalancer.- The generated admin password is not the working login. The upstream
LinuxServer image ships a built-in default login (
admin/admin123); theCALIBRE_ADMIN_PASSWORDSecret Manager secret is provisioned for a stronger credential but is not wired into the container automatically. Change the password in the Calibre-Web UI on first sign-in. - A Cloud Storage bucket is created but unused by default.
create_cloud_storage = truealways provisions thestorage-suffixed bucket, but it is only mounted at/configwhenstateful_pvc_enabled = false(Deployment mode). With the default StatefulSet/PVC path, the bucket exists but is not attached to the workload. - Image version is pinned via an app-specific build ARG. The Dockerfile reads
CALIBREWEB_VERSION(not the genericAPP_VERSIONthe Foundation injects); whenapplication_version = "latest"the build is pinned to a known-good0.6.24.
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 Calibre-Web StatefulSet
Calibre-Web pods are scheduled on Autopilot, which bills for the CPU/memory the
pods actually request. The workload runs as a StatefulSet (not a Deployment)
so it gets a stable pod identity and an ordered restart sequence
(stateful_pod_management_policy defaults to OrderedReady when left null).
- Console: Kubernetes Engine → Workloads → select the Calibre-Web workload for pods, revisions, and events.
- CLI:
kubectl get statefulset,pods,svc -n "$NAMESPACE"
kubectl logs -n "$NAMESPACE" statefulset/<service-name> --tail=100
kubectl describe pod -n "$NAMESPACE" -l app=<service-name>
See App_GKE for how Autopilot, scaling, and workload type selection are managed.
B. Block storage — the /config Persistent Volume Claim
Calibre-Web's SQLite files (app.db, Calibre's metadata.db), configuration,
cache, and logs live on a per-pod block PVC mounted at /config
(stateful_pvc_mount_path), sized 20Gi by default
(stateful_pvc_size) on the standard-rwo (SSD pd-balanced) StorageClass
(stateful_pvc_storage_class).
- Console: Kubernetes Engine → Storage → Persistent Volume Claims; Compute Engine → Storage → Disks.
- CLI:
kubectl get pvc -n "$NAMESPACE"
kubectl describe pvc -n "$NAMESPACE" -l app=<service-name>
gcloud compute disks list --project "$PROJECT" --filter="name~<service-name>"
Because SSD PVCs draw the tight SSD_TOTAL_GB quota and scaling a stateful app to
zero keeps its PVC (only deleting the PVC/namespace reclaims the quota), verify
current usage before a wide campaign of stateful GKE apps. See App_GKE
for the StatefulSet and PVC lifecycle in general.
C. Cloud Storage
A Cloud Storage bucket (suffix storage) is always provisioned when
create_cloud_storage = true (the default) and the workload service account is
granted access — but it is only mounted into the pod at /config via GCS FUSE
when stateful_pvc_enabled = false (i.e. when running as a Deployment instead of
the default StatefulSet). With the default configuration the bucket exists but is
not attached to the running workload.
- Console: Cloud Storage → Buckets.
- CLI:
gcloud storage buckets list --project "$PROJECT" --filter="name~storage"
See App_GKE for GCS FUSE mounts and CMEK options.
D. Secret Manager
One Calibre-Web secret is generated automatically and stored in Secret Manager:
CALIBRE_ADMIN_PASSWORD (a 24-character random value, secret-<prefix>-<app>-admin-password),
injected into the container as a secret environment variable. On GKE, secrets are
projected via the Secret Store CSI driver / SecretSync; the key has no __, so it
is a valid targetKey.
- Console: Security → Secret Manager.
- CLI:
gcloud secrets list --project "$PROJECT" --filter="name~admin-password"
gcloud secrets versions access latest --secret=<admin-password-secret-name> --project "$PROJECT"
Note: the injected secret is not automatically applied as the Calibre-Web login — see §3 for the actual first-login credentials. See App_GKE for the Secret Store CSI integration and rotation.
E. Networking & ingress
service_type defaults to ClusterIP (internal-only), and
enable_custom_domain = true by default provisions a Kubernetes Gateway API
resource — but with the default empty application_domains = [] there is no
hostname for the Gateway to route, so the deployment has no external access
configured out of the box. To expose Calibre-Web externally, either set
application_domains to a real hostname, or switch service_type to
LoadBalancer (which additionally picks up the App_GKE default <reserved-ip>.nip.io
hostname when reserve_static_ip = true).
- Console: Network services → Load balancing / Gateways; VPC network → IP addresses.
- CLI:
kubectl get svc,gateway,httproute -n "$NAMESPACE"
gcloud compute addresses list --project "$PROJECT"
See App_GKE for custom domains, Cloud CDN, and static IP details.
F. Cloud Logging & Monitoring
Pod stdout/stderr flow to Cloud Logging; GKE metrics flow to Cloud Monitoring.
Uptime checks are disabled by default (uptime_check_config.enabled = false).
- 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. Calibre-Web Application Behaviour
- No initialization job by default.
initialization_jobsdefaults to[]; Calibre-Web manages its own SQLite storage and needs no database bootstrap (there is nodb-initjob becausedatabase_type = "NONE"). Only user-supplied jobs run. - Thin-wrapper image, no custom entrypoint. The Dockerfile is
FROM lscr.io/linuxserver/calibre-web:${CALIBREWEB_VERSION}with no added entrypoint script — the upstream LinuxServer s6-based init runs unchanged.image_source = "custom"is set purely so the Foundation builds/mirrors the image into Artifact Registry. - First-boot storage layout. The LinuxServer image drops privileges to
PUID=1000/PGID=1000and keeps all state under/config(app.db, Calibre'smetadata.db, config, cache, logs); the ebook library itself lives under/books(empty on first run — the in-app setup wizard points Calibre-Web at it).stateful_fs_groupdefaults to3000(the CalibreWeb Helm chart convention) so the PVC is group-writable. - Login credentials. The upstream image's built-in first-login credentials are
admin/admin123. The Secret-Manager-generatedCALIBRE_ADMIN_PASSWORDis not applied automatically — it exists so a strong password is available in Secret Manager and so a future image/entrypoint can consume it. Change the admin password in the Calibre-Web UI immediately after first sign-in. - Health probes. Both the startup and liveness probes issue an HTTP GET
/(Calibre-Web's login page), which returns200with no authentication required — so probes pass as soon as the server is serving, independent of any login state. Startup:initial_delay=15s,timeout=5s,period=10s,failure_threshold=10. Liveness:initial_delay=30s,timeout=5s,period=30s,failure_threshold=3. - Scaling constraint.
min_instance_count = 1/max_instance_count = 1by default. Because the StatefulSet provisions a distinct PVC per replica (volumeClaimTemplates), raisingmax_instance_countdoes not give pods a shared library — each pod gets its own independent, unsynchronised/config. - Verify the deployment:
kubectl get statefulset,pods,pvc -n "$NAMESPACE"
kubectl logs -n "$NAMESPACE" statefulset/<service-name> --tail=100
kubectl exec -n "$NAMESPACE" <pod-name> -- ls -la /config
4. Configuration Variables
Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for Calibre-Web are listed; every other input is inherited from App_GKE with its standard behaviour and defaults.
Group 3 — Application Identity
| Variable | Default | Description |
|---|---|---|
application_name | calibreweb | Base name for resources. Do not change after first deploy. |
application_version | latest | lscr.io/linuxserver/calibre-web tag used as the custom-build base; latest is pinned to a known-good tag (0.6.24) at build time via the app-specific CALIBREWEB_VERSION build ARG. |
application_display_name | Calibre-Web | Human-readable name for display purposes. |
description | Calibre-Web — a web app for browsing, reading and downloading ebooks from a Calibre library. | Populates the GKE workload description. |
Group 4 — Runtime & Scaling
| Variable | Default | Description |
|---|---|---|
cpu_limit | 1000m | 1 vCPU. |
memory_limit | 1Gi | Memory limit for the Calibre-Web container. |
min_instance_count / max_instance_count | 1 / 1 | Keep at 1 — see §3 scaling constraint (per-replica PVC forking). |
container_port | 8083 | Fixed by CalibreWeb_Common; the module variable is not forwarded to App_GKE and has no effect. |
enable_cloudsql_volume | false | Correctly left off — Calibre-Web does not use Cloud SQL. |
enable_image_mirroring | true | Mirrors the image into Artifact Registry to avoid Docker Hub rate limits. |
Group 6 — GKE Backend & Cluster
| Variable | Default | Description |
|---|---|---|
service_type | LoadBalancer | Externally reachable by default. Set ClusterIP for internal-only — see §2.E for the ingress gotcha that creates in combination with application_domains = []. |
workload_type | null → StatefulSet | Auto-resolved because stateful_pvc_enabled = true. |
session_affinity | None | No sticky routing by default (single replica makes this moot at default scale). |
Group 7 — StatefulSet Configuration
| Variable | Default | Description |
|---|---|---|
stateful_pvc_enabled | true | Provisions a block PVC per pod — required because gcsfuse would corrupt Calibre-Web's SQLite files. |
stateful_pvc_size | 20Gi | Size for /config plus the Calibre library. |
stateful_pvc_mount_path | /config | Where Calibre-Web stores its SQLite databases, config, cache, and logs. |
stateful_pvc_storage_class | standard-rwo (SSD) | Draws the regional SSD_TOTAL_GB quota. Calibre-Web's SQLite workload does not need SSD IOPS — consider standard (HDD pd-standard) to conserve SSD quota. |
stateful_fs_group | 3000 | Matches the CalibreWeb Helm chart convention so the PVC is group-writable by PUID:PGID=1000:1000. |
Group 9 — PodDisruptionBudget
| Variable | Default | Description |
|---|---|---|
enable_pod_disruption_budget | true | On by default (unlike most modules). |
pdb_min_available | 1 | With max_instance_count = 1, a PDB of 1 effectively blocks voluntary eviction of the sole pod until it can be rescheduled. |
Group 10 — Observability & Health
| Variable | Default | Description |
|---|---|---|
startup_probe / liveness_probe (from CalibreWeb_Common) | HTTP / | Login page, 200, no auth required — passes as soon as the server is serving. |
uptime_check_config.enabled | false | Uptime checks are off by default. |
Group 13 — Filesystem (NFS)
| Variable | Default | Description |
|---|---|---|
enable_nfs | false | NFS is not used by Calibre-Web — persistence is via the StatefulSet PVC (or GCS FUSE in Deployment mode) instead. |
network_tags | ["nfsserver"] | Defaults to including the nfsserver tag even though NFS is disabled by default; harmless but only relevant if you separately enable NFS. |
Group 14 — Cloud Storage
| Variable | Default | Description |
|---|---|---|
create_cloud_storage | true | Always provisions the storage-suffixed bucket, but see §2.C — it is unmounted while stateful_pvc_enabled = true (the default). |
Group 15 — Redis
| Variable | Default | Description |
|---|---|---|
enable_redis | true (declared) | Inert — main.tf hardcodes enable_redis = false on the Foundation call regardless of this variable's value. Calibre-Web does not use Redis. |
redis_host / redis_port | "" / 6379 | Declared for foundation-variable mirroring but not forwarded to App_GKE at all. |
Group 16 — Database
| Variable | Default | Description |
|---|---|---|
database_type | NONE | Fixed by CalibreWeb_Common; no Cloud SQL instance, database, or user is created. All other database_*/sql_* variables in this group are explicitly "Not referenced." |
Group 19 — Custom Domain, Static IP & Networking
| Variable | Default | Description |
|---|---|---|
enable_custom_domain | true | Provisions the Gateway API resource, but does nothing externally useful without application_domains set (see §2.E). |
application_domains | [] | Empty by default — set this to get a working external hostname while service_type = ClusterIP. |
reserve_static_ip | true | Ties into the App_GKE default <reserved-ip>.nip.io hostname only when service_type = LoadBalancer; behaviour when combined with the default ClusterIP is not explicitly documented by the Foundation. |
All other inputs follow standard App_GKE behaviour.
5. Outputs
These values are returned on a successful deployment and are the quickest way to locate and explore the running resources.
| Output | Description |
|---|---|
service_name | Kubernetes Service name. |
namespace | Namespace the workload runs in. |
service_cluster_ip | In-cluster ClusterIP. |
stage_service_cluster_ips | Map of ClusterIPs for stage-specific services. |
service_external_ip | External LoadBalancer IP (when a static IP is reserved and service_type = LoadBalancer). |
service_url | URL to reach Calibre-Web. |
calibreweb_admin_password_secret_id | Secret Manager secret ID holding the generated CALIBRE_ADMIN_PASSWORD (see §3 for why this is not the initial login). |
storage_buckets | Created Cloud Storage buckets. |
network_name / network_exists / regions | VPC network, presence, available regions. |
container_image / container_registry | Deployed image and Artifact Registry repo. |
monitoring_enabled / monitoring_notification_channels | Monitoring status and channels. |
deployment_id / tenant_id / resource_prefix | Naming identifiers. |
project_id / project_number | Project identifiers. |
initialization_jobs | Names of any user-supplied initialization jobs (none by default). |
statefulset_name | Name of the StatefulSet. |
cicd_enabled / cicd_configuration | CI/CD status and details (repo, trigger, registry). |
github_repository_url / github_repository_owner / github_repository_name | CI/CD GitHub details. |
artifact_registry_repository / cloudbuild_trigger_name / cloudbuild_trigger_id | Registry and build trigger. |
kubernetes_ready | Whether the cluster/workload is ready. |
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_mode | VPC-SC status. |
audit_logging_enabled / artifact_registry_cmek_enabled | Audit logging and CMEK status. |
6. Configuration Pitfalls & Sensible Defaults
Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).
Inherited plan-time validation. This module passes its configuration through the App_GKE foundation engine, which validates values and combinations at plan time — a
StatefulSetforced alongside a stateless setting, IAP with no authorized identities,quota_memory_*given as bare integers, an out-of-rangecontainer_port/backup_retention_days. Invalid configuration fails the plan with a clear, named error before any resource is created, so most mistakes below are caught up front rather than at apply or runtime.
| Setting | Sensible value | Risk | Consequence if wrong |
|---|---|---|---|
max_instance_count | 1 | Critical | Each StatefulSet replica gets its own PVC (volumeClaimTemplates) — scaling up forks /config into independent, unsynchronised copies per pod, not a shared library. |
stateful_pvc_enabled | true (keep) | Critical | Switching to false/Deployment mode moves /config onto GCS FUSE, which can corrupt Calibre-Web's SQLite files (app.db, metadata.db) under its access patterns. |
service_type + application_domains | Set application_domains, or use LoadBalancer | High | With the defaults (ClusterIP + empty application_domains), the deployment has no external access configured despite enable_custom_domain = true. |
stateful_pvc_storage_class | standard (HDD) for quota-constrained projects | Medium | The default standard-rwo (SSD) draws the tight SSD_TOTAL_GB quota; a campaign of stateful GKE apps can exhaust it around app #8. Scaling to zero does not free the PVC — only deleting it does. |
CALIBRE_ADMIN_PASSWORD (auto-generated) | Change the login in the UI on first sign-in | High | The generated secret is not applied automatically; the working first-login credential is the upstream default admin/admin123 until changed manually. |
enable_resource_quota | Understand before enabling | Medium | This module's quota_cpu_*/quota_memory_*/quota_max_* variables are declared but not forwarded to App_GKE — enabling the quota uses the Foundation's own defaults (4 CPU / 4Gi–8Gi memory / 20 pods), not any value set on this module. |
enable_redis | Ignore — inert | Low | main.tf hardcodes enable_redis = false regardless of this variable; Calibre-Web has no Redis dependency. |
create_cloud_storage | false if you don't need the fallback bucket | Low | With the default StatefulSet path the storage bucket is provisioned but never mounted — a small, avoidable cost if you don't plan to switch to Deployment mode. |
memory_limit | 1Gi (raise for large libraries) | Medium | Calibre-Web loads library/collection indexes into memory; undersizing risks OOM under a large Calibre library. |
quota_memory_requests / _limits (foundation-level, if enabling quotas directly on App_GKE) | binary units (4Gi, 8192Mi) | Critical | Bare integers are treated as bytes and block all pod scheduling in the namespace. |
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. Calibre-Web-specific application configuration shared across platform variants is described in CalibreWeb_Common.
Related guides
- Hands-on lab: Calibre-Web on GKE Autopilot — deploy it step by step, with the console screens and commands at each stage.
- Calibre-Web on Google Cloud Run — the same application on Cloud Run, for when you need the other deployment target.
- Calibre-Web Common — Shared Application Configuration — the configuration shared by both deployment targets.
- Deployed alongside Komga on GKE Autopilot, Kavita on GKE Autopilot, Audiobookshelf on GKE Autopilot, Navidrome on GKE Autopilot in the Digital Library solution.
Need RAD to do something it does not do yet? Request it on the roadmap, or vote on what is already there.