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
| Area | Provided by Kimai_Common | Where it surfaces |
|---|---|---|
| Secrets | Generates APP_SECRET (Symfony CSRF/session signing key) and ADMINPASS (initial admin password), both stored in Secret Manager | Injected as container secret env vars; retrieve via Secret Manager (see below) |
| Container image | A thin custom build FROM kimai/kimai2:<tag> with a runtime DATABASE_URL-composing wrapper entrypoint; built via Cloud Build | container_image output of the platform deployment |
| Database engine | Fixes Cloud SQL for MySQL 8.0 (MYSQL_8_0) as the engine | §Database in the platform guides |
| Database bootstrap | Defines a single db-init job (creates the database, user, and grants) — no separate migrate job | initialization_jobs output |
| Core settings | Sets ADMINMAIL and the lowercase memory_limit env var; leaves DATABASE_URL entirely to runtime composition | Application behaviour in the platform guides |
| Object storage | A storage GCS bucket, optionally GCS-FUSE-mounted at /opt/kimai/var/data | storage_buckets output |
| Health checks | Declares 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 assecret-<prefix>-<app>-app-secret. This is Kimai's Symfony CSRF/session signing key. The vendor image would otherwise generate and persist one itself undervar/data/.appsecreton 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 assecret-<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:
- 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.
- 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 withphp -r 'echo rawurlencode(...)'— the image has nopython3installed; 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 wrapperexecs the vendor's own, completely unmodified entrypoint chain (docker-php-entrypoint /entrypoint.sh), which in turn runskimai: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_IPresolves to the raw Cloud SQL private IP (Kimai_CloudRunsetsenable_cloudsql_volume = false— no Auth Proxy socket volume is mounted). - On GKE,
DB_IPresolves to the cloud-sql-proxy sidecar's127.0.0.1loopback (Kimai_GKEsetsenable_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'scaching_sha2_passworddefault requires--get-server-public-keyfor 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 andkimai:installrun 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.