Certification track: Associate Cloud Engineer (ACE)
Umami Common Shared Configuration Module
The Umami Common module defines the Umami analytics platform configuration for the RAD Modules ecosystem. It is a shared configuration and secrets module — it creates Secret Manager secrets and produces a config output consumed by platform-specific wrapper modules (Umami CloudRun and Umami GKE).
1. Overview
Purpose: To centralise all Umami-specific configuration (container image, PostgreSQL database setup, environment variable mapping, health probes, application secret, and initialization job) in a single module shared by both Cloud Run and GKE deployments.
Architecture:
Layer 3: Application Wrappers
├── Umami_CloudRun ──┐
└── Umami_GKE ──┤── instantiate Umami_Common
↓
Umami_Common (this module)
Creates: Secret Manager secret (APP_SECRET)
Produces: config, secret_ids, storage_buckets, path
↓
Layer 2: Platform Modules
├── App_CloudRun (serverless deployment)
└── App_GKE (Kubernetes deployment)
↓
Layer 1: App_Common (networking, database, storage, secrets, IAM)
Key characteristics:
- Generates one Secret Manager secret:
APP_SECRET(injected as theAPP_SECRETenvironment variable) — the Next.js application secret key used by Umami for session signing and security. - Stateless analytics service: Umami stores all data in PostgreSQL. No GCS storage buckets are provisioned.
- No Redis required: Umami uses only PostgreSQL.
storage_bucketsreturns an empty list. /api/heartbeathealth endpoint: All probes target Umami's built-in heartbeat endpoint rather than a generic root path.- Database migrations are run automatically by Umami itself at container startup via Prisma — the
db-initjob only pre-creates the database and user.
2. Outputs
config
The application configuration object passed to the platform module via application_config.
| Field | Value / Description |
|---|---|
app_name | "umami" |
application_version | Version tag (default: "postgresql-latest") |
container_image | "ghcr.io/umami-software/umami" (GitHub Container Registry source image) |
image_source | "custom" — a wrapper image is built via Cloud Build to map DB_* variables to DATABASE_URL |
enable_image_mirroring | true (hardcoded) — mirrors the image from GitHub Container Registry into Artifact Registry |
container_build_config | dockerfile_path = "Dockerfile", context_path = ".", build_args = { UMAMI_VERSION = <version> } |
container_port | 3000 |
database_type | "POSTGRES_15" — Umami requires PostgreSQL |
db_name | Database name (default: "umami") |
db_user | Database user (default: "umami") |
enable_cloudsql_volume | Whether to mount the Cloud SQL instance as a Unix socket volume (default: true) — native socket integration on Cloud Run, cloud-sql-proxy sidecar on GKE |
cloudsql_volume_mount_path | "/cloudsql" |
container_resources | CPU: 1000m, Memory: 512Mi — Umami is lightweight |
min_instance_count | var.min_instance_count (default 1) |
max_instance_count | var.max_instance_count (default 10) |
environment_variables | Passed through from var.environment_variables |
initialization_jobs | Default db-init job (when var.initialization_jobs = []), or the user-supplied list |
startup_probe | HTTP GET /api/heartbeat, 30s initial delay, 10s timeout, 10s period, 30 failure threshold |
liveness_probe | HTTP GET /api/heartbeat, 30s initial delay, 10s timeout, 30s period, 3 failure threshold |
secret_ids
Map of environment variable names to Secret Manager secret IDs:
{
APP_SECRET = "secret-<tenant_resource_prefix>-<application_name>-app-secret"
}
This map is passed directly as module_secret_env_vars to the Foundation Module and injected into the container at runtime as APP_SECRET.
storage_buckets
Returns an empty list []. Umami is a stateless analytics service — all data lives in PostgreSQL. No dedicated application storage buckets are provisioned.
path
The absolute path to the module directory, used by wrapper modules to locate the scripts/ directory.
3. Secret Created
| Secret ID Pattern | Description |
|---|---|
secret-<tenant_resource_prefix>-<application_name>-app-secret | 32-character alphanumeric APP_SECRET for Umami (no special characters). Injected as APP_SECRET. |
The secret is created with replication { auto {} } (automatic multi-region replication). A propagation wait is inserted after creation to prevent race conditions when the Cloud Run service or GKE pod first reads the secret at startup.
4. Input Variables
Application
| Variable | Type | Default | Description |
|---|---|---|---|
project_id | string | — | GCP project ID. Required. |
application_name | string | "umami" | Application name. Used as a base name for resources. |
application_version | string | "postgresql-latest" | Umami image tag. Must be a postgresql- prefixed tag. |
display_name | string | "Umami" | Human-readable display name. |
description | string | "Umami - Privacy-focused web analytics" | Application description. |
tenant_id | string | "demo" | Unique identifier for the deployment environment. Used in secret IDs. |
resource_labels | map(string) | {} | Common labels to apply to all resources. |
db_name | string | "umami" | PostgreSQL database name. |
db_user | string | "umami" | PostgreSQL application user. |
cpu_limit | string | "1000m" | Container CPU limit included in the config output. |
memory_limit | string | "512Mi" | Container memory limit included in the config output. |
environment_variables | map(string) | {} | Additional environment variables passed directly to the container. |
initialization_jobs | list(any) | [] | Custom init jobs. Empty triggers the default db-init job. |
startup_probe | object | (see §5) | Startup health probe configuration. |
liveness_probe | object | (see §5) | Liveness health probe configuration. |
min_instance_count | number | 1 | Minimum number of running instances. |
max_instance_count | number | 10 | Maximum number of running instances. |
Storage & Volumes
| Variable | Type | Default | Description |
|---|---|---|---|
enable_cloudsql_volume | bool | true | Mount the Cloud SQL instance's Unix socket (native integration on Cloud Run; cloud-sql-proxy sidecar on GKE). |
region | string | "us-central1" | GCP region (used as bucket location if storage were provisioned). |
5. Health Probes
Umami exposes /api/heartbeat as its dedicated health endpoint. This endpoint returns HTTP 200 when Umami is fully initialised and connected to PostgreSQL.
| Probe | Path | Initial Delay | Timeout | Period | Failure Threshold | Purpose |
|---|---|---|---|---|---|---|
| Startup | /api/heartbeat | 30s | 10s | 10s | 30 | Allows up to 5 minutes total for Umami to start and run Prisma migrations on a fresh database |
| Liveness | /api/heartbeat | 30s | 10s | 30s | 3 | Restarts the container if Umami becomes unresponsive |
The startup probe's high failure_threshold (30 × 10s = 300s after the initial delay) accommodates fresh database provisioning where Prisma migrations must run and apply the full Umami schema. Subsequent restarts are faster because migrations are already applied.
Umami exposes /api/heartbeat specifically for health checks — use this path rather than the generic root path / for probe configuration.
6. Initialization Job
One db-init job runs by default (when initialization_jobs = []):
| Field | Value |
|---|---|
| Image | postgres:15-alpine |
| Script | scripts/create-db-and-user.sh |
| Secrets required | DB_PASSWORD (application user password), ROOT_PASSWORD (postgres superuser) |
execute_on_apply | true |
| Timeout | 600s, 1 retry |
create-db-and-user.sh behaviour:
- Connects to Cloud SQL PostgreSQL via the mounted Unix socket (
-h <socket-dir>, usingpsql's native socket-directory support — no Auth Proxy sidecar on Cloud Run). - Creates the
umamidatabase user with the password from Secret Manager. - Creates the
umamidatabase if it does not exist. - Grants the
umamiuser full privileges on the database.
Note: Umami runs its own Prisma-based database migrations on container startup. The db-init job only pre-creates the database shell and user — Umami populates the schema itself. This means the db-init job must complete successfully before the Umami container starts.
7. Scripts and Container Image
All supporting files are in scripts/. The scripts/ directory is used as the Docker build context when container_image_source = "custom".
Dockerfile
Wraps the official ghcr.io/umami-software/umami:<version> image:
- Accepts
UMAMI_VERSIONas a build argument. - Copies a custom entrypoint script that assembles
DATABASE_URLfrom platform-injected DB_* environment variables. - Exposes port
3000.
Entrypoint
The custom entrypoint runs before the Umami process to:
-
Construct
DATABASE_URL: Assembles the PostgreSQL connection string fromDB_USER,DB_PASSWORD,DB_HOST,DB_PORT, andDB_NAME— the standard variables injected by the platform'sApp_CloudRun/App_GKEmodules. WhenDB_HOSTis a Unix socket path (starts with/), the entrypoint unconditionally resolves it to TCP127.0.0.1, because socket paths cannot appear in apostgresql://URL. This substitution is only correct on GKE, where a realcloud-sql-proxysidecar listens on TCP127.0.0.1:5432. Cloud Run's Cloud SQL integration (enable_cloudsql_volume=true) is the native socket mount (run.googleapis.com/cloudsql-instances) — there is no Auth Proxy sidecar and no TCP localhost listener on Cloud Run — so on that platform the substitution yieldsECONNREFUSED 127.0.0.1:5432and a startup-probe failure. The password is URL-encoded so special characters in the generated secret do not break Prisma's URL parser. -
Set
DATABASE_URL: Exports the assembled connection string asDATABASE_URLfor Umami's Prisma ORM. -
Start Umami: Locates and
execs the Umami Node server (/app/server.js, falling back to the Next.js standalone server oryarn start).
This approach avoids exposing a plaintext connection string as an environment variable or storing it in Terraform state.
8. DATABASE_URL Assembly
Umami uses the DATABASE_URL environment variable for all database connectivity. The platform injects individual DB_* variables:
| Platform env var | Role |
|---|---|
DB_HOST | PostgreSQL host (a Unix socket directory when enable_cloudsql_volume=true — the native Cloud Run socket integration on Cloud Run, or the cloud-sql-proxy sidecar's socket on GKE) |
DB_USER | PostgreSQL application username |
DB_PASSWORD | PostgreSQL application password (from Secret Manager) |
DB_NAME | PostgreSQL database name |
DB_PORT | PostgreSQL port (5432 for standard connections) |
The custom entrypoint assembles these into DATABASE_URL using the format:
postgresql://DB_USER:DB_PASSWORD@DB_HOST:DB_PORT/DB_NAME
When DB_HOST is a Unix socket path (the default, via enable_cloudsql_volume=true), the entrypoint unconditionally substitutes 127.0.0.1 as the host, so the URL stays parseable:
postgresql://DB_USER:DB_PASSWORD@127.0.0.1:5432/DB_NAME
This substitution is only valid on GKE. There, a real cloud-sql-proxy sidecar listens on TCP 127.0.0.1:5432 in addition to its Unix socket, so redirecting to loopback works. On Cloud Run, the Cloud SQL socket is the native run.googleapis.com/cloudsql-instances integration — there is no Auth Proxy sidecar and no TCP 127.0.0.1 listener at all — so the same substitution instead produces ECONNREFUSED 127.0.0.1:5432 and the container fails its startup probe. Do not treat "the Auth Proxy also listens on TCP localhost" as a platform-universal fact when reusing or modifying this entrypoint; verify the actual injected DB_HOST/DB_IP on the deployed revision before assuming.
This is why container_image_source = "custom" is the recommended default — the "prebuilt" mode using the official Umami image requires manually providing a fully-formed DATABASE_URL in environment_variables.
9. Platform-Specific Differences
| Aspect | Umami CloudRun | Umami GKE |
|---|---|---|
min_instance_count | 0 (scale-to-zero supported) | 1 (always at least one pod running) |
max_instance_count | 3 (Cloud Run default) | 10 (GKE HPA default) |
DB_HOST | Native Cloud Run Cloud SQL socket path (/cloudsql/..., no Auth Proxy sidecar) | Cloud SQL private IP or cloud-sql-proxy sidecar socket |
| Health probe registration | Cloud Run startup/liveness probe configuration | Kubernetes probe spec via App GKE |
| Uptime checks | Disabled by default (uptime_check_config.enabled = false) | Disabled by default (uptime_check_config.enabled = false) |
| Redis | Not used (enable_redis = false) | Not used (the mirrored enable_redis declaration defaults true but is not forwarded to the Foundation) |
| Storage buckets | None (empty list returned) | None (empty list returned) |
10. Implementation Pattern
# Example: how Umami_CloudRun instantiates Umami_Common
module "umami_app" {
source = "../Umami_Common"
project_id = var.project_id
application_name = var.application_name
application_version = var.application_version
tenant_id = var.tenant_id
db_name = var.application_database_name
db_user = var.application_database_user
cpu_limit = var.cpu_limit
memory_limit = var.memory_limit
description = var.application_description
startup_probe = var.startup_probe
liveness_probe = var.liveness_probe
enable_cloudsql_volume = var.enable_cloudsql_volume
initialization_jobs = var.initialization_jobs
}
# The wrapper assembles the four locals the Foundation Module consumes
# (config is merged with wrapper-level overrides such as image_source)
locals {
application_modules = { umami = merge(module.umami_app.config, { image_source = var.container_image_source }) }
module_env_vars = {}
module_secret_env_vars = module.umami_app.secret_ids
module_storage_buckets = module.umami_app.storage_buckets
}
# config is passed to App_CloudRun via application_config
module "app_cloudrun" {
source = "../App_CloudRun"
application_config = local.application_modules
module_env_vars = local.module_env_vars
module_secret_env_vars = local.module_secret_env_vars
module_storage_buckets = local.module_storage_buckets
scripts_dir = abspath("${module.umami_app.path}/scripts")
# ... other inputs
}
11. First-Login Instructions
After deployment, navigate to the Umami service URL. The default credentials are:
| Field | Value |
|---|---|
| Username | admin |
| Password | umami |
Change these immediately after first login. The default credentials are publicly known and will expose your analytics dashboard if left unchanged.
To change the admin password:
- Log in with the default credentials.
- Navigate to Settings → Profile.
- Update the username and password.
- Click Save.
12. Adding Websites for Tracking
After logging in:
- Navigate to Settings → Websites.
- Click Add website.
- Enter the website name and domain.
- Click Save to generate a tracking ID.
- Embed the tracking script in the target website's
<head>:
<script async src="https://<your-umami-url>/script.js" data-website-id="<your-website-id>"></script>
The tracking script collects page views, sessions, referrers, browser and device information, and custom events without using cookies or storing personal data.
13. Custom Events API
Umami supports custom event tracking via a client-side function call or direct API:
// Track a custom event
umami.track('button-click', { label: 'sign-up', page: '/home' });
Custom events appear in the Umami dashboard under Events. Use them to track conversions, button clicks, form submissions, or any user interaction.
The Umami REST API (/api) also allows programmatic retrieval of analytics data for integration with dashboards or reporting tools. Authenticate with a bearer token obtained from POST /api/auth/login.
14. Team Workspaces
Umami supports multiple users, multiple websites, and role-based access control:
- Admin: Full access to all settings, users, and websites.
- View-only: Read access to analytics dashboards.
To add team members:
- Navigate to Settings → Users.
- Click Create user.
- Assign the appropriate role.
Multiple websites can be tracked within a single Umami instance — each website gets its own tracking ID and isolated analytics view.
Related guides
- Umami on Google Cloud Run — this configuration deployed on Cloud Run.
- Umami GKE Module — Configuration Guide — this configuration deployed on GKE.