Skip to main content

Kimai Common — Shared Application Configuration

Kimai_Common is the shared application layer for Kimai. It is not deployed on its own; instead it supplies the Kimai-specific configuration that both Kimai_GKE and Kimai_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 Kimai, see the platform guides (Kimai_GKE, Kimai_CloudRun) and the foundation guides (App_GKE, App_CloudRun, App_Common).


1. What this layer provides

AreaProvided by Kimai_CommonWhere it surfaces
SecretsGenerates APP_SECRET (Symfony CSRF/session signing key) and ADMINPASS (initial admin password), both stored in Secret ManagerInjected as container secret env vars; retrieve via Secret Manager (see below)
Container imageA thin custom build FROM kimai/kimai2:<tag> with a runtime DATABASE_URL-composing wrapper entrypoint; built via Cloud Buildcontainer_image output of the platform deployment
Database engineFixes Cloud SQL for MySQL 8.0 (MYSQL_8_0) as the engine§Database in the platform guides
Database bootstrapDefines a single db-init job (creates the database, user, and grants) — no separate migrate jobinitialization_jobs output
Core settingsSets ADMINMAIL and the lowercase memory_limit env var; leaves DATABASE_URL entirely to runtime compositionApplication behaviour in the platform guides
Object storageA storage GCS bucket, optionally GCS-FUSE-mounted at /opt/kimai/var/datastorage_buckets output
Health checksDeclares startup_probe/liveness_probe defaults (GET /en/login) that both variants inherit as-is§Observability in the platform guides

2. Secrets in Secret Manager

Two secrets are generated automatically and stored in Secret Manager — neither is ever set in plain text:

  • APP_SECRET — a 32-character random string (no special characters), stored as secret-<prefix>-<app>-app-secret. This is Kimai's Symfony CSRF/session signing key. The vendor image would otherwise generate and persist one itself under var/data/.appsecret on first boot; providing it explicitly via Secret Manager means no persistent volume is required just to keep this value stable across container restarts.
  • ADMINPASS — a 24-character random string (no special characters), stored as secret-<prefix>-<app>-admin-password. This is the password for the bootstrapped super-admin account (see §4).

Both are injected as secret env vars via config.secret_environment_variables (APP_SECRET, ADMINPASS), and the module waits 30 seconds (time_sleep.wait_for_secrets) after creating both secret versions before returning its config output, to let Secret Manager global replication catch up. The database password is generated and managed separately by the foundation; its secret name is reported in the platform deployment outputs (database_password_secret).

Retrieve either secret after deployment:

gcloud secrets list --project "$PROJECT" --filter="name~kimai"
gcloud secrets versions access latest --secret=<app-secret-or-admin-password-name> --project "$PROJECT"

See App_Common for the shared secret and Workload Identity model.


3. Why DATABASE_URL is composed at runtime, not set in Terraform

Kimai's Doctrine DBAL layer is configured through a single environment variable, DATABASE_URL, in the form mysql://user:pass@host:port/db?charset=utf8mb4&serverVersion=8.0 — not discrete DB_HOST/DB_USER/DB_PASSWORD/DB_NAME variables the way many other frameworks in this catalogue expect. Two constraints make this impossible to set directly as a plan-time Terraform value:

  1. The database password is a runtime secret. It is generated by the Foundation at apply time and is not known when Terraform renders the container spec.
  2. Cloud Run does not interpolate $(VAR) references the way Kubernetes does. A plan-time string like "mysql://$(DB_USER):$(DB_PASSWORD)@..." would reach the Cloud Run container as that literal, un-substituted text — not a working connection string.

Kimai_Common sets image_source = "custom" and builds a thin wrapper image FROM kimai/kimai2:${KIMAI_VERSION} (see scripts/Dockerfile) whose only addition is scripts/entrypoint.sh, copied in as /cloud-entrypoint.sh and set as the image's ENTRYPOINT. This entrypoint runs before anything else in the container and composes DATABASE_URL at that point, when the real, Foundation-injected DB_PASSWORD secret is actually present as an environment variable:

ENCODED_DB_PASS=$(php -r 'echo rawurlencode(getenv("DB_PASSWORD") ?: "");')
export DATABASE_URL="mysql://${DB_USER}:${ENCODED_DB_PASS}@${DB_IP}:3306/${DB_NAME}?charset=utf8mb4&serverVersion=8.0"
exec docker-php-entrypoint /entrypoint.sh
  • Password URL-encoding. Generated MySQL/Cloud SQL passwords can contain characters (@ : / ? # % & +) that would otherwise be misparsed as URL delimiters inside the DSN. The wrapper URL-encodes the password with php -r 'echo rawurlencode(...)' — the image has no python3 installed; it is a PHP image, so the fix uses what's already present rather than adding a new interpreter.
  • Verified locally before the cloud. This exact behaviour — the URL-encoding step and the vendor's own DB-wait preflight check inside docker-php-entrypoint /entrypoint.sh — was confirmed end-to-end by building the wrapper image and running it locally against a real MySQL container with a password deliberately containing special characters (p@ss:w/rd?1), before ever deploying to Cloud Run or GKE.
  • Unmodified handoff. After exporting DATABASE_URL, the wrapper execs the vendor's own, completely unmodified entrypoint chain (docker-php-entrypoint /entrypoint.sh), which in turn runs kimai:install (schema creation and migrations) and the admin-user bootstrap on every boot.

4. Why DB_IP, not the standard DB_HOST

The calling Application Module sets db_host_env_var_name = "DB_IP", so the Foundation injects the database host under both the standard DB_HOST name and this DB_IP alias. entrypoint.sh reads $DB_IP deliberately, not $DB_HOST:

  • On Cloud Run, DB_IP resolves to the raw Cloud SQL private IP (Kimai_CloudRun sets enable_cloudsql_volume = false — no Auth Proxy socket volume is mounted).
  • On GKE, DB_IP resolves to the cloud-sql-proxy sidecar's 127.0.0.1 loopback (Kimai_GKE sets enable_cloudsql_volume = true — the Foundation injects a Cloud SQL Auth Proxy sidecar container into the pod).

Either way, DB_IP is a plain host with no colons — safe to place directly in a URL authority (mysql://user:pass@HOST:3306/db). The standard DB_HOST variable can instead take the Cloud SQL Unix socket-directory form on Cloud Run (e.g. /cloudsql/project:region:instance), which contains colons that would break URL-authority parsing if used the same way — this is precisely why the wrapper reads DB_IP and not DB_HOST.


5. Admin bootstrap

The vendor's own entrypoint.sh (inside the base kimai/kimai2 image, run at the end of the wrapper's exec chain) runs kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS" on every container start whenever ADMINPASS is set. This is idempotent — a no-op once the account already exists, so it is safe to run on every boot rather than as a one-time init job.

The account username is always admin, hardcoded by the vendor's own entrypoint regardless of what ADMINMAIL (var.admin_email) is set to. Only the email and password are configurable through this module's inputs.


6. Database bootstrap

Kimai needs only one default initialization job — a materially simpler model than catalogue apps that separate schema-creation from an explicit migrate step:

  • db-init (mysql:8.0-debian, scripts/db-init.sh, execute_on_apply = true, max_retries = 3, timeout_seconds = 600) — creates the MySQL database and application user. This is the catalogue's standard, proven MySQL boilerplate (shared with Matomo/SnipeIT/BookStack), needed specifically because Cloud SQL MySQL 8's caching_sha2_password default requires --get-server-public-key for RSA key exchange over plain TCP — something the Foundation's own automatic role/database creation does not handle.

There is no separate migrate job. kimai:install (schema creation and migrations) runs on every container boot as part of the vendor's own entrypoint chain — idempotently, per the vendor image's documented behaviour — so this one db-init job plus the Foundation's automatic MySQL role/database creation is sufficient; Kimai's own boot sequence handles the rest.

Both the job and the boot-time install are safe to re-run. Inspect the database directly with:

gcloud sql connect <instance-name> --user=<db-user> --database=<db-name> --project "$PROJECT"

The instance, database, and user names are in the platform deployment outputs.


7. Health probe behaviour

Both Kimai_CloudRun and Kimai_GKE pass this layer's own default startup_probe/liveness_probe values straight through unchanged:

  • Startup: HTTP GET /en/login, 30s initial delay, 10s timeout, 15s period, 20-retry failure threshold — generous, to cover the container's own DB-wait preflight and kimai:install run on first boot.
  • Liveness: HTTP GET /en/login, 60s initial delay, 10s timeout, 30s period, 3-retry failure threshold.

/en/login (Kimai's login page) returns 200 once the application is ready. / also works for a generic probe (Kimai issues a 302 redirect from the root path), but /en/login is the more precise, directly-verified target and is what both platform variants actually configure.


8. Object storage

This layer provisions one GCS bucket via storage_buckets (name_suffix = "storage", STANDARD class, force_destroy = true, public access prevention enforced). When enable_gcs_storage_volume = true (the default), this bucket is GCS-FUSE-mounted into the container at /opt/kimai/var/data, where Kimai stores uploaded invoice logos/templates and plugin data.

This is a separate bucket from the generic storage_buckets input each Application Module also exposes at the Foundation level (defaulting to a bucket named data in both Kimai_CloudRun and Kimai_GKE) — that generic bucket is not read or written by Kimai unless explicitly wired up.


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