Skip to main content

Radicale Common — Shared Application Configuration

Radicale_Common is the shared application layer for Radicale. It is not deployed on its own; instead it supplies the Radicale-specific configuration that both Radicale_GKE and Radicale_CloudRun build on, so the two platform variants behave identically where it matters. End users never configure this layer directly — it has no deployment UI inputs of its own — but understanding what it provides explains the defaults you see in the platform docs.

For the infrastructure that actually provisions and runs Radicale, see the platform guides (Radicale_GKE, Radicale_CloudRun) and the foundation guides (App_GKE, App_CloudRun, App_Common).


1. What this layer provides

AreaProvided by Radicale_CommonWhere it surfaces
Container imageThin-wraps ghcr.io/kozea/radicale with a custom build so the cloud entrypoint can be layered incontainer_image output of the platform deployment
Database engineNone — Radicale is a pure filesystem store (database_type = "NONE")§3 below and the platform guides
AuthenticationNo built-in default admin account — generates a real ADMIN_PASSWORD secret and hashes it into an htpasswd file on every boot§2 below
Object storageDeclares the Cloud Storage storage bucket that backs /var/lib/radicalestorage_buckets output
Collection seedingDefines the default seed-default-collections init job that works around Cloud Run's MKCOL restriction§4 below
Health checksSupplies the default startup/liveness probes targeting /§Observability in the platform guides

2. No default admin account — a real generated secret

Radicale is unusual among the applications in this catalogue in that it has no upstream default credential to warn about at all. Its auth.type setting defaults to denyall — every request is rejected — until an htpasswd file is explicitly configured. There is no "admin/admin" or well-known first-login password baked into the image.

Radicale_Common closes this gap itself:

  • Generates a real, random 24-character random_password.admin_password.
  • Stores it in Secret Manager as secret-<wrapper_prefix>-admin-password.
  • Outputs it as secret_ids = { ADMIN_PASSWORD = ... } (forwarded as module_secret_env_vars) and secret_values = { ADMIN_PASSWORD = ... } (forwarded as module_explicit_secret_values / explicit_secret_values for the GKE SecretSync path).
  • Also injects a plain ADMIN_USERNAME environment variable, defaulting to admin.

The cloud entrypoint (baked into the custom image, see §4 of the platform guides and scripts/entrypoint.sh) hashes ADMIN_PASSWORD with bcrypt (Python's bcrypt module, already bundled in the official image's venv) into an htpasswd-format line, and rewrites both the htpasswd file and the Radicale INI config on every container boot — not just on first boot. Radicale has no user table to check "is this already initialized?" against, so regenerating every boot is the simplest design that (a) self-heals a lost/corrupted htpasswd file and (b) picks up a rotated ADMIN_PASSWORD immediately on the next restart, with no separate reconciliation step needed.

gcloud secrets versions access latest --secret=secret-<wrapper_prefix>-admin-password --project "$PROJECT"

3. Database engine — none

Radicale does not use a database of any kind — SQL, NoSQL, or embedded. Every calendar and address book ("collection") is a plain directory on disk containing one iCalendar (.ics) or vCard (.vcf) file per item, plus a .Radicale.props JSON metadata file describing the collection itself. This is Radicale's documented, stable on-disk format (the multifilesystem storage backend), not a private implementation detail.

The real on-disk path has one easy-to-miss extra segment, confirmed against radicale/storage/multifilesystem/base.py's _get_collection_root_folder():

<filesystem_folder>/collection-root/<username>/<collection-name>/

filesystem_folder is set to /var/lib/radicale/collections by the cloud entrypoint. Consequently:

  • database_type = "NONE" — no Cloud SQL instance, database, or user is created for Radicale.
  • There is no db-init job of any kind.
  • No PostgreSQL extensions, no MySQL plugins, and no Redis are involved (enable_redis is forced false by both Application Modules).

4. Container image and the custom build

Unlike apps in this catalogue that deploy an official image directly (container_image_source = "prebuilt"), Radicale_Common uses a custom, thin-wrapper build:

ARG RADICALE_VERSION=3.7.7
FROM ghcr.io/kozea/radicale:${RADICALE_VERSION}

USER root
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER radicale

ENTRYPOINT ["/entrypoint.sh"]
CMD ["--hosts", "0.0.0.0:5232,[::]:5232"]

The build exists solely to layer in the cloud entrypoint; there is no other application code changed.

  • App-specific build ARG. The Dockerfile reads RADICALE_VERSION, not the generic APP_VERSION the Foundation injects (which would force the resolved tag to latest). When application_version = "latest", the Common layer pins the build to RADICALE_VERSION=3.7.7; otherwise the requested version passes straight through.
  • GHCR tags have no v prefix. Radicale's GitHub Releases page shows tags like v3.7.7, but the container image on ghcr.io/kozea/radicale is published without the v (3.7.7). Building against the v-prefixed tag fails with MANIFEST_UNKNOWN — confirmed during an early build attempt for this module.
  • Cloud entrypoint responsibilities. Radicale has no native environment-variable configuration — it only reads an INI config file, located via the RADICALE_CONFIG env var (confirmed in radicale/__main__.py) or a compiled-in default path. entrypoint.sh writes that INI file plus the htpasswd file on every boot (see §2), then hands off to the image's own CLI entrypoint (/app/bin/python /app/bin/radicale).

5. The MKCOL / Cloud Run problem

This is the single most consequential platform-specific behaviour in this module — the result of three separate live-deployment debugging passes to isolate root cause at each layer.

The problem. Creating a new collection (a calendar or address book) via the standard CalDAV/CardDAV protocol requires the WebDAV MKCOL HTTP method. Confirmed live:

  • On Cloud Run, Google's frontend (GFE) rejects MKCOL at the edge with a generic "400 Bad Request" Google error page — the request never reaches the Radicale container at all. Every other method tested (GET, PUT, PROPFIND) passes through fine; this restriction is specific to MKCOL. Cloud Run services also have no shell/exec access, so there is no way for an operator to manually create a collection after the fact either. Without a workaround, a fresh Radicale_CloudRun deployment would be unable to create any calendar — via a standard client (Apple Calendar, Thunderbird, DAVx5) or even Radicale's own web UI, which also issues MKCOL internally.
  • On GKE, a plain L4 LoadBalancer Service does not have this restriction — MKCOL works natively (confirmed live: 201 Created).

The fix — seed-default-collections. Radicale_Common defines a default initialization job (execute_on_apply = true, running scripts/seed-default-collections.sh inside a plain alpine:3 container) that creates a "Default Calendar" and "Default Address Book" for the admin user by writing the directory structure and .Radicale.props metadata file directly onto the storage volume — a plain container with direct filesystem access, no HTTP/GFE layer involved at all. Verified live: after this job runs, a PROPFIND on the admin user's principal correctly lists both seeded collections with the right CalDAV/CardDAV resourcetypes, and a real calendar event (VEVENT) can be PUT and GET successfully.

The GKE + PVC caveat. The seed job is a shared Cloud-Run/GKE Common-module job and only mounts the GCS storage bucket (mount_gcs_volumes = ["storage"]) — it cannot attach to a StatefulSet's block PVC (a Kubernetes Job can't mount a ReadWriteOnce PVC already held by a running Pod). So on Radicale_GKE with stateful_pvc_enabled = true (the recommended production setting), the seed job's writes land in the otherwise-unused GCS bucket and the default collections do not appear automatically. This is harmless (no error, no crash) — it just means GKE+PVC operators should create their first calendar via a real CalDAV client or a direct curl -X MKCOL call (confirmed working) instead of expecting the pre-seeded defaults. On GKE without a PVC (GCS-backed Deployment mode — not the recommended production configuration), the seed job's writes land in the same bucket the app uses, so the defaults do appear there too.

If you supply your own initialization_jobs, this default is replaced entirely — you are then responsible for seeding (or not seeding) collections yourself.


6. Health probe behaviour

Both the startup and liveness probes issue an HTTP GET /. Radicale has no dedicated health endpoint; the root path returns an unauthenticated 302 redirect to its web UI (/.web) — confirmed live — and both Cloud Run and Kubernetes treat any 2xx–3xx response as a healthy probe result.

  • Startup probeinitial_delay = 15s, timeout = 5s, period = 10s, failure_threshold = 10.
  • Liveness probeinitial_delay = 30s, timeout = 5s, period = 30s, failure_threshold = 3.

7. Object storage and single-instance operation

A single Cloud Storage bucket is declared here and provisioned by the foundation:

  • name_suffix = "storage", storage class STANDARD, with public_access_prevention = "enforced".
  • On Cloud Run it always backs /var/lib/radicale via GCS FUSE.
  • On GKE it backs /var/lib/radicale via GCS FUSE unless stateful_pvc_enabled = true, in which case a real block PVC takes over the same mount path (enable_gcs_storage_volume is automatically set to false to avoid a double-mount) and the GCS bucket goes unused as a mount (though the seed job may still write into it — see §5).
gcloud storage buckets list --project "$PROJECT" --filter="name~radicale"

Why single-instance matters here. Radicale's multifilesystem storage backend uses OS-level file locking, which GCS FUSE does not reliably support. max_instance_count is pinned to 1 on both platforms specifically so this never becomes a real concurrency problem — Radicale itself documents a multifilesystem_nolock storage variant as explicitly single-process-only, confirming this is a genuine upstream constraint, not a cautious platform default. For a production deployment, prefer Radicale_GKE with stateful_pvc_enabled = true, which gives Radicale real POSIX file locking instead of GCS FUSE's weaker semantics.


For the Radicale-specific, user-facing configuration (variables by group, outputs, and how to explore each service from the Console and CLI), see the platform guides: Radicale_GKE and Radicale_CloudRun.