Skip to main content

Grocy on GKE Autopilot

Grocy on GKE Autopilot

Grocy is a self-hosted grocery and household ERP: inventory tracking with barcode scanning, chore/task management, shopping lists, and meal planning. It is distinct from this catalogue's Mealie module, which covers recipe/meal-planning only — Grocy is the broader household-ERP tool. This module deploys Grocy on GKE Autopilot on top of the App_GKE foundation, which provisions and manages the shared Google Cloud and Kubernetes infrastructure; Grocy_GKE is a thin wrapper that supplies Grocy's own configuration (image, port, probes, storage wiring) and forwards everything else straight through.

This guide focuses on the cloud services Grocy uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics common to every GKE application — Workload Identity, ingress and load balancing, 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.


What Grocy costs on RAD, and how that compares​

Running Grocy on GKE Autopilot pod in your own Google Cloud project costs about $42.08/month in Google Cloud charges, on top of a one-off 75-credit RAD module fee charged once at deploy time. Grocy is a lightweight household ERP with its own embedded SQLite database, so this deployment's running cost is almost entirely the small NFS file server behind it, not a managed database. In a RAD-managed project the module fee drops to 67.5 credits (10% lower) and the running cost is metered hourly in credits instead of billed directly by Google — see Pause it for free below for the one RAD-managed advantage a self-hosted VPS cannot match — or see the Cloud Run guide for the lower-cost option.

What you pay on RAD​

Own projectRAD-managed project
Module fee (one-off)75 credits67.5 credits (10% lower)
Build time~3-6 credits per build, either way~3-6 credits per build, either way
Google Cloud running cost$42.08/month, billed by Google at list price~67 credits/day, metered at list price plus RAD's margin

Default running cost in your own project

ResourceUS$/month (Google list price, us-central1)
GKE Autopilot pod, 1x vCPU / 1 GiB$36.08
Cloud NAT and networking$5.00
Cloud Storage (add-ons, backups)$1.00
GKE cluster management feeshared across every GKE app in the project; $0 if this is your only cluster, else ~73
Total$42.08

The GKE cluster management fee is excluded from the total above because it is shared across every GKE app in the project — $0 if this is the only one, otherwise roughly $73/month split across however many you run.

How it compares​

  • A self-managed comparison: Grocy's 1 vCPU/1 GiB footprint is light enough for a cheaper, smaller VPS class than the mid-tier boxes usually quoted here — but for a direct reference point, Hetzner CPX22/DigitalOcean 2 vCPU/4GB run about $24/mo and a GCP e2-standard-2 about $49/mo.
  • Grocy has no official hosted SaaS offering — it ships only as self-hosted software, so this is a VPS-vs-RAD-managed-stack comparison, not RAD vs. a subscription.
  • What a bare VPS doesn't give you: automatic backups of the SQLite data file, secret rotation, or monitoring — RAD's managed stack covers those even though the underlying database here is just a file.

Pause it for free: delete a RAD-managed project, restore it when you need it​

This is the headline advantage of a RAD-managed project, and it costs nothing while paused. Deleting a RAD-managed project unlinks its billing first, then asks Google to delete the project outright — Google's own 30-day recoverable soft delete. Unlike deleting one module, this does not tear down Cloud SQL, any VM, or the compute resource one by one: the whole project simply stops, and nothing is charged while it waits, because billing is already unlinked.

Restoring, within 30 days and only by the project's owner, asks Google to undelete the project and reattaches its billing account, then asks you to run Update on each deployment to confirm everything came back. Because nothing was individually destroyed, that Update finds the same resources already there — it is a check, not a rebuild, and an Update never charges the module fee again. This costs only a handful of credits (under US$1) in total for a typical 2-3-deployment chain.

What this needs: you must own the project (not one RAD only manages billing for), you must restore it yourself within 30 days — after that Google deletes it for good — and restoring is admitted like creating a new project, so your purchased credit balance must still clear the tier's floor (100 credits for the sandbox tier most study/demo use fits). Google says most services are fully working again within 36 hours of a restore.

One real gap: nightly backups are written to a bucket inside the project, and that bucket has Cloud Storage's soft-delete explicitly turned off, so it is very likely gone as soon as you delete the project — even though the project itself is recoverable for 30 days. If you've customised Grocy and want to keep that work, copy a backup out (to Google Drive, or a bucket outside the project) before deleting. For a default install with nothing irreplaceable in it, this does not matter.

Pay only while you use it, the other way: delete and redeploy​

In your own project, or once 30 days have passed on a RAD-managed one, there is no free pause — deleting removes the resources for good, and bringing Grocy back means redeploying from scratch. That costs about 78-81 credits (the 75-credit module fee plus a build).

Deleting only saves money once Grocy would otherwise sit unused long enough to clear that redeploy cost against its own running cost — for Grocy, that's about 6 days or more. Most of that running cost is usually the database (and, where this module uses one, the shared file-server VM or GKE cluster) — those stop only once nothing else in the project uses them, so deleting this app while something else shares the project saves only its own compute share.

Keep data first: nightly backups go to a bucket inside the deployment and are deleted with it, so copy the latest backup out before deleting if you want to keep it.

Lab sessions and Managed Environments​

Lab sessions, for training: a trainer runs a session for a class. Each participant gets the app in their own Google Cloud project for 15 minutes to 24 hours, within an allowance the trainer sets. Either the trainer funds every place, or each participant pays for their own. Everything is deleted when the session ends and unused credits go back to the trainer.

Managed Environments, for consultancies: a partner runs the app for a client from a ring-fenced wallet it funds, and settles with the client directly. If the wallet runs low, billing pauses and the data is kept, so nobody receives an unexpected charge. At the end the partner hands the project over and the deployments become the client's own.

Credits can be bought in more than 20 currencies, including XAF, XOF, NGN, GHS, KES and ZAR, by card, bank transfer or mobile money.

Sources (8 October 2026): Google Cloud Billing Catalog API list prices; radmodules.dev/pricing for RAD's own fees and daily-credit estimates; Google's project delete/restore documentation for the pause/restore claims.

1. Overview​

Grocy runs as an nginx + php-fpm container (the upstream LinuxServer.io grocy image, unmodified) on GKE Autopilot as a StatefulSet. The deployment wires together a focused set of Google Cloud services:

CapabilityGoogle Cloud serviceNotes
ComputeGKE Autopilot (StatefulSet)1 vCPU / 1 GiB by default, min=max=1 (no autoscaling — single-writer SQLite)
DatabaseNoneGrocy uses an embedded SQLite database — no Cloud SQL instance is created
Persistent stateBlock-storage PVC (standard-rwo, 20Gi)/config (SQLite database, config, uploads, backups) is mounted on a real block device, per-pod via volumeClaimTemplates — see §4
Object storageCloud StorageA storage bucket is provisioned but unused when the PVC is enabled (the default)
CacheNoneenable_redis is hardcoded false in main.tf — Grocy has no cache dependency
SecretsSecret ManagerNone generated for Grocy — no injectable admin credential exists
IngressKubernetes ServiceLoadBalancer by module default; this deployment uses ClusterIP (see §5)

Sensible defaults worth knowing up front:

  • SQLite is the only database Grocy supports. Confirmed by reading Grocy's own upstream source (services/DatabaseService.php) — there is no MySQL/Postgres driver branching at all. database_type is fixed to NONE.
  • A real block PVC, not NFS or GCS FUSE — by design, not as a bug fix. stateful_pvc_enabled = true is the module default, auto-resolving workload_type to "StatefulSet". Grocy_Common sets enable_gcs_storage_volume = !stateful_pvc_enabled, so the GCS-FUSE volume is skipped entirely whenever the PVC is used (the default state). See §4 for why this matters and how it compares to the Cloud Run variant.
  • Single instance only. min_instance_count = 1, max_instance_count = 1. Grocy's SQLite database is single-writer with no clustering support — and because the StatefulSet uses volumeClaimTemplates, raising the replica count would give each pod its own unsynchronised PVC rather than sharing /config.
  • No injectable admin credential. The upstream image ships default admin / admin credentials, changed via the web UI on first login. No Secret Manager secret is created for Grocy.
  • Health probes hit /, not /health. Grocy has no dedicated health endpoint; the login page (200, unauthenticated) is used for both startup and liveness probes.

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. Service and resource names are reported in the deployment Outputs.

A. GKE Autopilot — the Grocy StatefulSet​

Grocy runs as a single-replica StatefulSet (not a Deployment) so its volumeClaimTemplates can bind a stable, per-pod block PVC.

  • Console: Kubernetes Engine → Workloads → select the StatefulSet.
  • CLI:
    kubectl get statefulset -n "$NAMESPACE"
    kubectl get pods -n "$NAMESPACE" -o wide
    kubectl describe statefulset <name> -n "$NAMESPACE"

See App_GKE for StatefulSet vs. Deployment mechanics, rollout strategy, and Autopilot resource requests/limits.

B. The /config block-storage PVC​

All of Grocy's state — the embedded SQLite database (grocy.db), config.php, uploaded images/attachments, and backups — lives under /config, mounted on a real standard-rwo (Balanced PD) block volume via the StatefulSet's volumeClaimTemplates. This is the load-bearing storage decision for this module (see §4); losing or misconfiguring this PVC loses all Grocy data.

  • Console: Kubernetes Engine → Storage → Persistent Volume Claims.
  • CLI:
    kubectl get pvc -n "$NAMESPACE"
    kubectl describe pvc <pvc-name> -n "$NAMESPACE"

C. Cloud Storage​

A dedicated Cloud Storage storage bucket is provisioned automatically, but by default it is not used to back /config — that mount goes through the block PVC instead (see §4). It remains available for any custom gcs_volumes an operator adds.

  • Console: Cloud Storage → Buckets.
  • CLI:
    gcloud storage buckets list --project "$PROJECT" --filter="name~grocy"

D. Networking & ingress​

The module default is a LoadBalancer Service with an optional custom domain via Gateway API. This deployment's config/deploy.tfvars overrides this to service_type = "ClusterIP" with reserve_static_ip = false (a per-project static-IP quota constraint) — access it via kubectl port-forward or from inside the cluster.

  • Console: Kubernetes Engine → Gateways, Services & Ingress.
  • CLI:
    kubectl get svc -n "$NAMESPACE" -o wide
    kubectl port-forward -n "$NAMESPACE" svc/<service-name> 8080:80

See App_GKE for Gateway API, static IPs, and custom domains.

E. Cloud Logging & Monitoring​

Container logs flow to Cloud Logging; Kubernetes metrics flow to Cloud Monitoring, with optional uptime checks and alert policies.

  • Console: Logging → Logs Explorer; Monitoring → Dashboards / Alerting.
  • CLI:
    kubectl logs -n "$NAMESPACE" <pod-name> --tail=100

3. Grocy Application Behaviour​

  • No first-deploy database setup. Grocy has no external database and no db-init job — it creates and migrates its own embedded SQLite schema under /config on first boot.
  • /config durability depends on the PVC, not a managed database. Because Grocy's entire state (database, config, uploads, backups) lives in files on /config, the correctness of the block PVC is the correctness of the deployment. Confirm the PVC is bound and writable before trusting any data written to it.
  • No admin credential is generated or injectable. The upstream image ships default admin / admin credentials. Log in with those on first access and change the password immediately via Users → admin → Edit in the Grocy UI — there is no environment variable or Secret Manager value that sets this for you.
  • Health path. Both the startup and liveness probes issue HTTP GET /, which returns Grocy's login page (200) with no authentication required. This is not a dedicated health endpoint — Grocy has none — but it reliably indicates the nginx + php-fpm stack is serving.
  • Single-writer constraint is architectural, not a scaling knob. Because Grocy's SQLite database has no MySQL/Postgres equivalent and no clustering support, max_instance_count must stay at 1. There is no configuration that safely enables horizontal scaling for this module.
  • Verified live. Pod grocygkee6a1e84d-0 reported 1/1 Running, 0 restarts. Boot logs show a clean s6-overlay/LinuxServer startup — migrations succeeded, self-signed TLS keys generated under /config/keys with no permission errors, [ls.io-init] done.. kubectl exec ... curl against GET / returned 302 → Location: /stockoverview → followed to GET /login → 200, body containing <title>Login | Grocy</title> (10,102 bytes) — matching the Cloud-Run-verified login page exactly.

4. Why This Module Uses a Block PVC — a Clean-Slate Design, Not a Bug Fix​

This module's persistent-storage wiring is worth understanding by contrast with its sibling, Grocy_CloudRun.

On Cloud Run, Grocy originally mounted /config on a GCS-FUSE-backed bucket — this catalogue's usual pattern for persistent app config — and it crash-looped in production. Grocy writes to data/grocy.db-journal on every database transaction (roughly every 1–2 seconds under light use), and GCS FUSE's object-storage translation layer, built around eventual-consistency whole-object semantics, could not sustain that write frequency. The Cloud Run fix was to switch /config to a Cloud Filestore (NFS) mount instead — see the Grocy_CloudRun guide, §4 for the full story.

Grocy_GKE never had that problem to fix, because it was designed with a different storage primitive from the start: stateful_pvc_enabled = true is the module's own default, which runs Grocy as a StatefulSet with a genuine block device (standard-rwo, a Balanced Persistent Disk) mounted per-pod at /config. A block PVC is not a network filesystem and not an object-storage translation layer — from the container's point of view it behaves exactly like a local disk, with real POSIX semantics (rename, fsync, byte-range locks) and no FUSE layer sitting between the SQLite engine and the bytes on disk. There is nothing here for a high write-frequency journal file to overwhelm.

Grocy_Common encodes this relationship directly: enable_gcs_storage_volume = !stateful_pvc_enabled — so whenever the PVC is enabled (the default), the GCS-FUSE volume that caused the Cloud Run bug is never even wired in. This also means the separate GKE-specific gcsfuse UID/GID mount-permission bug hit elsewhere in this catalogue (confirmed on modules like PeerTube_GKE, where a non-root container can't write to a gcsfuse mount without explicit uid/gid mount options) simply doesn't apply here either — there is no gcsfuse mount to misconfigure.

stateful_fs_group = 1000 matches the LinuxServer.io image's real PUID/PGID (1000/1000), so the block PVC comes up group-writable for the container's actual process — confirmed live: the pod's boot log shows self-signed TLS keys generated under /config/keys with zero permission errors.

The result: this module was deployed and verified with zero bugs found or fixed. Where Grocy_CloudRun's story is "the wrong storage primitive broke SQLite, and here's the fix," Grocy_GKE's story is simpler — the right storage primitive for a single-writer SQLite workload was chosen from the start, and nothing broke.


5. Configuration Variables​

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

Group 3 — Application Identity​

VariableDefaultDescription
application_namegrocyBase name for resources. Do not change after first deploy.
application_display_nameGrocyHuman-readable name shown in the Console.
application_versionlatestLinuxServer image tag. "latest" resolves to the pinned v4.6.0-ls333 (the GROCY_VERSION build ARG, not the generic APP_VERSION).

Group 4 — Runtime & Scaling​

VariableDefaultDescription
cpu_limit / memory_limit1000m / 1GiGrocy is lightweight; the memory default gives headroom for image/attachment uploads.
min_instance_count1Keeps the pod warm — avoids cold starts.
max_instance_count1Must stay at 1. Grocy's SQLite database is single-writer with no clustering support, and a StatefulSet's volumeClaimTemplates gives each additional replica its own unsynchronised PVC.
container_port80Grocy's default HTTP port.
enable_cloudsql_volumefalseGrocy has no Cloud SQL — keep false.

Group 6 — GKE Backend Configuration​

VariableDefaultDescription
workload_typenullAuto-resolves to "StatefulSet" because stateful_pvc_enabled = true by default.
service_typeLoadBalancerModule default for a browser-facing app. This deployment overrides to ClusterIP via config/deploy.tfvars (per-project static-IP quota constraint).

Group 7 — StatefulSet Configuration​

VariableDefaultDescription
stateful_pvc_enabledtrueThe core design decision for this module — see §4. A real block-storage PVC, not a network filesystem or GCS FUSE.
stateful_pvc_size20GiSized to hold the SQLite database, images/attachments, and backups under /config.
stateful_pvc_mount_path/configGrocy keeps all state (database, config, uploads, backups) here. Do not change unless the upstream image's data path changes.
stateful_pvc_storage_classstandard-rwoBalanced Persistent Disk — a genuine block device, no FUSE translation layer.
stateful_fs_group1000Matches Grocy's real PUID/PGID (1000/1000) — confirmed live via successful TLS-key generation under /config/keys.

Group 13 — Filesystem (NFS)​

VariableDefaultDescription
enable_nfsfalseNot used on GKE, unlike Grocy_CloudRun which relies on NFS. The block PVC replaces that role here.

Group 15 — Redis​

VariableDefaultDescription
enable_redistrue (variable default)Overridden unconditionally to false in main.tf regardless of this variable's value — Grocy has no cache dependency.

Group 16 — Database Backend​

VariableDefaultDescription
database_typeNONEFixed — Grocy has no SQL database of any kind.

Group 11 — Workload Automation​

VariableDefaultDescription
initialization_jobs[]No default init job — Grocy bootstraps its own SQLite schema on first boot.

Group 10 — Observability & Health​

VariableDefaultDescription
startup_probeHTTP /, 15s delay, 10 retriesGrocy's login page — no dedicated health endpoint exists.
liveness_probeHTTP /, 30s delay, 3 retriesSame endpoint as the startup probe.

All other inputs are inherited from App_GKE with standard behaviour.


6. Outputs​

OutputDescription
service_name / namespace / service_cluster_ipKubernetes Service identity.
service_external_ip / service_urlExternal address (if LoadBalancer and a static IP are in use).
statefulset_nameName of the StatefulSet running Grocy.
storage_bucketsCreated Cloud Storage buckets (the storage bucket, unused for /config when the PVC is enabled).
network_name / network_exists / regionsVPC network, presence, regions.
container_image / container_registryDeployed image and Artifact Registry repo.
monitoring_enabled / monitoring_notification_channelsMonitoring status and channels.
deployment_id / tenant_id / resource_prefixNaming identifiers.
project_id / project_numberProject identifiers.
initialization_jobsCreated initialization job names (empty by default).
kubernetes_readyWhether the cluster endpoint is available and all Kubernetes resources are deployed.
cicd_enabled / github_repository_url / github_repository_owner / github_repository_name / cicd_configurationCI/CD status and details.
artifact_registry_repository / cloudbuild_trigger_name / cloudbuild_trigger_idRegistry and build trigger.
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_modeVPC-SC status.
audit_logging_enabled / artifact_registry_cmek_enabledAudit logging and CMEK status.

7. 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. Most out-of-range or contradictory inputs are caught before any resource is created.

SettingSensible valueRiskConsequence if wrong
stateful_pvc_enabledtrueCriticalDisabling it without a matching gcs_volumes mount reverts /config to GCS FUSE, reproducing the write-frequency corruption risk confirmed on Grocy_CloudRun — a network/object-storage translation layer cannot sustain Grocy's grocy.db-journal write pattern.
stateful_pvc_mount_path/configCriticalGrocy hardcodes its data path to /config. Changing the mount path without a matching image change loses access to the database, config, and uploads.
max_instance_count1CriticalGrocy's SQLite database is single-writer with no clustering support. Because the StatefulSet uses volumeClaimTemplates, any value above 1 doesn't even share storage across replicas — each pod gets its own disconnected copy of the data.
stateful_fs_group1000HighGrocy runs as UID 1000 / GID 1000 (LinuxServer PUID/PGID). A mismatched fsGroup leaves the PVC unwritable by the container's actual process, producing permission errors on first boot.
enable_redisAny value — ignoredLowmain.tf hardcodes enable_redis = false regardless of this variable, so setting it true has no effect. Not a risk, just a no-op worth knowing about.
database_typeNONEMediumGrocy ignores this entirely (no code path reads it), but setting anything else provisions an unused, billed Cloud SQL instance.
Admin passwordChange on first loginHighThe upstream image's default admin / admin credentials are publicly documented; leaving them unchanged on a LoadBalancer-exposed deployment is a real exposure.
min_instance_count1LowSetting 0 saves cost but reintroduces cold starts on Grocy's nginx + php-fpm stack.

For the foundation behaviour referenced throughout — Workload Identity, ingress and load balancing, autoscaling, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, backups, and image mirroring — see App_GKE. Grocy-specific application configuration shared with the Cloud Run variant is described in Grocy_Common. For the storage-corruption bug this module avoided by design, see Grocy_CloudRun.

Need RAD to do something it does not do yet? Request it on the roadmap, or vote on what is already there.