Skip to main content

Certification track: Associate Cloud Engineer (ACE)

Penpot on Google Cloud Run

Penpot on Google Cloud Run

This document provides a comprehensive reference for the modules/Penpot_CloudRun module. It covers architecture, IAM, configuration variables, Penpot-specific behaviours, and operational patterns for deploying Penpot on Google Cloud Run (v2).


1. Module Overview

Penpot is an open-source design and prototyping platform — a self-hosted alternative to Figma. It provides vector design editing, interactive prototyping, component libraries, design tokens, developer handoff with code inspection, and real-time multiplayer collaboration via WebSockets. Penpot CloudRun is a wrapper module built on top of App CloudRun. It uses App CloudRun for all GCP infrastructure provisioning and injects Penpot-specific application configuration, multi-service architecture, and asset storage configuration via Penpot Common.

Key Capabilities:

  • Compute: Cloud Run v2 (Gen2), three coordinated services — backend (Clojure HTTP API + WebSocket server), frontend (nginx serving React SPA), and exporter (headless Chromium for PDF/PNG/SVG export). Backend defaults to 2 vCPU / 2 Gi.
  • Data Persistence: Cloud SQL PostgreSQL 15. GCS assets bucket for design assets (fonts, images, thumbnails), accessed natively via Workload Identity / ADC.
  • Real-Time Collaboration: Redis is mandatory as the WebSocket pub/sub event bus. All backend replicas share event state via Redis DB 0. Without Redis, real-time multiplayer editing breaks when more than one backend instance is running.
  • Security: Inherits Cloud Armor WAF, IAP, Binary Authorization, and VPC Service Controls from App CloudRun. No application-level secrets are auto-generated by Penpot Common.
  • Protocol: container_protocol defaults to 'http1'; set 'h2c' (HTTP/2 cleartext) for end-to-end WebSocket multiplexing.
  • CI/CD: Cloud Build custom image pipeline by default; Cloud Deploy progressive delivery optional.
  • Health Checks: Startup probe uses TCP on port 6060 (Penpot 2.x does not expose an unauthenticated HTTP health endpoint). The module also exposes startup_probe_config and health_check_config targeting /api/health for load balancer health checks.

Project & Application Identity

VariableGroupTypeDefaultDescription
project_id1stringGCP project ID. Required.
tenant_deployment_id2string'demo'Short suffix appended to all resource names.
support_users2list(string)[]Email recipients for monitoring alerts.
resource_labels2map(string){}Labels applied to all provisioned resources.
region1string'us-central1'GCP region for all resources.
application_name3string'penpot'Base resource name. Do not change after initial deployment.
display_name3string'Penpot - Open Source Design Tool'Human-readable name shown in the GCP Console.
description3string'Penpot - Open-source design and prototyping tool for teams'Cloud Run service description.
application_version3string'latest'Penpot image version tag applied to all three services.

Wrapper architecture: Penpot CloudRun calls Penpot Common to build the application configuration object containing Penpot-specific environment variables, probe configuration, and multi-service definitions for the backend, frontend, and exporter. module_storage_buckets carries the penpot-assets bucket provisioned by Penpot Common. scripts_dir resolves to abspath("${module.penpot_app.path}/scripts") at apply time.

PostgreSQL note: Penpot requires PostgreSQL 15. database_type = "POSTGRES_15" is the default and must not be changed to MySQL or SQL Server — Penpot's Clojure backend only supports PostgreSQL.


2. IAM & Access Control

Penpot_CloudRun delegates all IAM provisioning to App_CloudRun. The Cloud Run SA, Cloud Build SA, IAP service agent, and password rotation role sets are identical to those in App_CloudRun.

No application-level secrets: Penpot Common does not auto-generate application secrets (no equivalent of SECRET_KEY or DIRECTUS_KEY). Penpot manages its own internal signing keys at runtime. The DB_PASSWORD secret is provisioned automatically by App CloudRun. User-defined secrets — including an SMTP password — can be added via secret_environment_variables.

GCS asset access: The Penpot backend reads and writes design assets (images, fonts, thumbnails) to the penpot-assets GCS bucket. Access is granted via the Cloud Run SA's Workload Identity binding — no explicit credentials are required. PENPOT_STORAGE_BACKEND=gcs and PENPOT_STORAGE_GCS_BUCKET_NAME are injected automatically.

120-second IAM propagation delay: Inherited from App CloudRun — the Penpot services are not deployed until the delay completes, preventing secret-read failures on the first revision start.

For the complete role tables and IAP, password rotation, and public access details, see the App_CloudRun documentation.


3. Core Service Configuration

A. Compute (Cloud Run)

Penpot's backend is a Clojure JVM application. Cold starts require the JVM to initialise and the application to run PostgreSQL migrations, which can take 60–120 seconds on first boot. min_instance_count defaults to 0 (scale-to-zero, cost-first); set it to 1 to keep the backend warm for real-time collaboration where users expect instant connectivity.

Container protocol: container_protocol defaults to 'http1'. Set 'h2c' (HTTP/2 cleartext) to enable WebSocket multiplexing over the same connection for real-time multiplayer editing.

Multi-service deployment: The frontend (nginx, port 80) and exporter (headless Chromium) are deployed as additional Cloud Run services alongside the main backend. The PENPOT_PUBLIC_URI variable is automatically set to the predicted frontend service URL. For custom domains, update PENPOT_PUBLIC_URI via environment_variables to match the actual URL users access.

VariableGroupDefaultDescription
deploy_application4trueSet false for infrastructure-only deployment (PostgreSQL, GCS, secrets).
cpu_limit4'2000m'CPU per backend instance. 2 vCPU minimum for JVM + WebSocket handling.
memory_limit4'2Gi'Memory per backend instance. Increase to 4 Gi for large teams or complex designs.
min_instance_count40Minimum backend instances. Raise to 1 or higher for real-time collaboration.
max_instance_count41Maximum backend instances.
container_port46060Penpot backend HTTP + WebSocket port. Do not change.
execution_environment4'gen2'Gen2 required for NFS mounts and GCS Fuse.
timeout_seconds43600Max request duration. Long timeout accommodates large export operations.
enable_cloudsql_volume4trueConnects to Cloud SQL via Unix socket via Auth Proxy sidecar.
container_protocol4'http1'Set 'h2c' for end-to-end HTTP/2 WebSocket multiplexing.
enable_image_mirroring4trueMirrors Penpot images into Artifact Registry to avoid Docker Hub rate limits.
traffic_split4[]Percentage-based canary/blue-green traffic allocation.
service_annotations4{}Advanced Cloud Run annotations.
service_labels4{}Labels applied to the Cloud Run service.

Differences from App CloudRun defaults:

VariableApp CloudRunPenpot CloudRunReason
container_port80806060Penpot backend's native port.
memory_limit'512Mi''2Gi'JVM requires more memory headroom than typical Node.js/Python apps.
enable_image_mirroringfalsetruePenpot images are pulled from Docker Hub; mirroring avoids rate limits.
timeout_seconds3003600Large export operations (PDF/PNG of complex designs) can take several minutes.

B. Penpot Feature Flags & JVM Tuning

VariableGroupDefaultDescription
penpot_flags5'enable-registration enable-login disable-demo-users'Space-separated Penpot feature flags. Key flags: enable-registration (allow self-registration), disable-registration (invite-only), enable-login-with-password, enable-oidc-<provider>.
jvm_max_heap5'1g'JVM maximum heap. Set to roughly half of memory_limit. For a 2 Gi container, '1g' is appropriate; for 4 Gi, use '2g'.
jvm_min_heap5'512m'JVM initial heap. Controls how much memory the JVM reserves at startup.

C. Database (Cloud SQL — PostgreSQL 15)

Penpot requires PostgreSQL 15. database_type = "POSTGRES_15" is the default. Unlike Ghost (MySQL), all standard PostgreSQL features are available. Penpot runs its own database migrations at startup — no separate db-init job is needed or provided by Penpot Common.

VariableGroupDefaultDescription
db_name13'penpot'PostgreSQL database name. Do not change after initial deployment.
db_user13'penpot'PostgreSQL application user. Password auto-generated and stored in Secret Manager.
database_type13'POSTGRES_15'Cloud SQL database engine. Do not change — Penpot requires PostgreSQL.
database_password_length1332Auto-generated password length. Range: 16–64.
enable_auto_password_rotation6falseAutomated zero-downtime password rotation.
rotation_propagation_delay_sec690Seconds to wait after rotation before restarting services.

D. Storage (GCS Assets Bucket)

Penpot Common automatically provisions a penpot-assets GCS bucket for design assets (fonts, user-uploaded images, thumbnails). The backend accesses this bucket via PENPOT_STORAGE_BACKEND=gcs and ADC/Workload Identity — no explicit credentials are required.

NFS is also available (enable_nfs = true) for shared file storage. When redis_host is not provided, the module falls back to using the NFS server IP as the Redis host.

VariableGroupDefaultDescription
create_cloud_storage12trueSet false to skip additional bucket creation. The penpot-assets bucket from Penpot Common is always provisioned.
storage_buckets12[]Additional GCS buckets beyond the auto-provisioned assets bucket.
enable_nfs12trueProvisions an NFS volume for shared storage. Also used as the fallback Redis host.
nfs_mount_path12'/mnt/nfs'Container path where the NFS share is mounted.
gcs_volumes12[]GCS buckets to mount via GCS Fuse (requires gen2).
manage_storage_kms_iam12falseCreates a CMEK KMS keyring and enables CMEK on storage buckets.
enable_artifact_registry_cmek12falseCreates an Artifact Registry KMS key for at-rest image encryption.

E. Networking

VariableGroupDefaultDescription
ingress_settings8'all''all' — public internet; 'internal' — VPC only; 'internal-and-cloud-load-balancing' — forces traffic through the HTTPS LB.
vpc_egress_setting8'PRIVATE_RANGES_ONLY''PRIVATE_RANGES_ONLY' routes only RFC 1918 traffic via VPC.

F. Initialization

Penpot runs its own PostgreSQL migrations at backend startup. No separate db-init Cloud Run Job is injected by Penpot Common — the backend's Clojure process applies migrations automatically before the application accepts traffic. Allow 60–120 seconds for first boot.

Custom initialization jobs and cron jobs are still supported via the initialization_jobs and cron_jobs variables.

VariableGroupDefaultDescription
initialization_jobs15[]One-shot Cloud Run Jobs run during deployment. Leave empty — Penpot handles its own migrations.
cron_jobs15[]Recurring scheduled Cloud Run Jobs. Each entry: name, schedule, image, command, args, env_vars, secret_env_vars, cpu_limit, memory_limit, timeout_seconds, max_retries, task_count, parallelism, mount_nfs, mount_gcs_volumes, script_path, paused.

4. Advanced Security

A. Cloud Armor WAF

When enable_cloud_armor = true, a Global HTTPS Load Balancer with a Cloud Armor WAF policy (OWASP Top 10, adaptive DDoS, 500 req/min rate limiting) is provisioned in front of Cloud Run.

VariableGroupDefaultDescription
enable_cloud_armor14falseProvisions Global HTTPS LB + Cloud Armor WAF. Required for custom domains and DDoS protection.
admin_ip_ranges14[]CIDR ranges exempted from WAF rules (e.g., design team VPN, CI/CD egress IPs).

B. Identity-Aware Proxy (IAP)

When enable_iap = true, Google identity authentication is required before requests reach Penpot. Useful for restricting access to internal design teams without managing Penpot user accounts for every visitor.

VariableGroupDefaultDescription
enable_iap8falseEnables IAP natively on the Cloud Run service.
iap_authorized_users8[]Users/service accounts granted access. Format: 'user:email' or 'serviceAccount:sa@...'.
iap_authorized_groups8[]Google Groups granted access. Format: 'group:name@example.com'.

C. Binary Authorization

When enable_binary_authorization = true, Cloud Run enforces that deployed Penpot images carry a valid cryptographic attestation.

VariableGroupDefaultDescription
enable_binary_authorization10falseEnforces image attestation. Requires a Binary Authorization policy pre-configured in the project.

D. VPC Service Controls

When enable_vpc_sc = true, all GCP API calls from this module are bound within an existing VPC-SC perimeter.

VariableGroupDefaultDescription
enable_vpc_sc18falseRegisters module API calls within the project's VPC-SC perimeter. A perimeter must already exist.
vpc_cidr_ranges18[]VPC subnet CIDR ranges for the VPC-SC network access level. Auto-discovered when empty.
vpc_sc_dry_run18trueLogs VPC-SC violations without blocking. Set false to enforce.
organization_id18""GCP Organization ID override for VPC-SC. Auto-discovered from project when empty.
enable_audit_logging18falseEnables detailed Cloud Audit Logs (DATA_READ, DATA_WRITE, ADMIN_READ).

E. Secret Manager Integration

Penpot secrets are stored in Secret Manager and injected natively by Cloud Run at revision start.

VariableGroupDefaultDescription
secret_environment_variables6{}Map of env var name → Secret Manager secret ID. Use for SMTP passwords, OIDC client secrets, etc.
secret_rotation_period6'2592000s'Secret Manager rotation notification frequency. Default: 30 days.
secret_propagation_delay630Seconds to wait after secret creation before dependent resources proceed.

5. Traffic & Ingress

A. HTTPS Load Balancer

When enable_cloud_armor = true, a Global HTTPS Load Balancer backed by a Serverless NEG is provisioned. Traffic flows: Internet → Cloud Armor → Global HTTPS LB → Serverless NEG → Cloud Run.

Setting ingress_settings = 'internal-and-cloud-load-balancing' forces all Penpot traffic through the LB, preventing direct *.run.app URL access. This is recommended for production deployments where the load balancer IP is the canonical entry point.

B. Cloud CDN

When enable_cdn = true (requires enable_cloud_armor = true), Cloud CDN is attached to the HTTPS Load Balancer backend.

Penpot consideration: Penpot serves a mix of authenticated design API calls and static frontend assets (React SPA, CSS, fonts). CDN is well-suited for the frontend static assets but should not cache API responses or WebSocket connections. The nginx frontend serves assets with appropriate cache headers; the backend API should not be CDN-cached.

VariableGroupDefaultDescription
enable_cdn14falseEnables Cloud CDN on the HTTPS LB backend.
max_images_to_retain147Maximum number of recent container images to keep in Artifact Registry.
delete_untagged_images14trueAutomatically deletes untagged images from Artifact Registry.
image_retention_days1430Days after which images are eligible for deletion.

C. Custom Domains

Custom domains are attached to the Global HTTPS Load Balancer via application_domains. Google-managed SSL certificates are provisioned automatically.

Important for Penpot: After adding a custom domain, set PENPOT_PUBLIC_URI via environment_variables to match the custom domain URL. Penpot uses PENPOT_PUBLIC_URI to generate links in email invitations and to route WebSocket connections. A mismatch causes broken invitation links and WebSocket connection failures.

VariableGroupDefaultDescription
application_domains14[]Custom domain names for the HTTPS LB. (e.g., ['design.example.com'])

6. CI/CD & Delivery

A. Cloud Build Triggers

When enable_cicd_trigger = true, a Cloud Build GitHub connection and push trigger are provisioned.

VariableGroupDefaultDescription
enable_cicd_trigger10falseProvisions a Cloud Build GitHub trigger.
github_repository_url10""Full HTTPS URL of the GitHub repository.
github_token10""GitHub PAT (repo, admin:repo_hook scopes). Sensitive.
github_app_installation_id10""GitHub App installation ID (preferred for organisation repos).
cicd_trigger_config10{ branch_pattern = "^main$" }Advanced trigger config: branch_pattern, included_files, ignored_files, trigger_name, substitutions.

B. Cloud Deploy Pipeline

When enable_cloud_deploy = true (requires enable_cicd_trigger = true), the CI/CD pipeline is upgraded to a managed Cloud Deploy delivery pipeline with sequential promotion stages.

VariableGroupDefaultDescription
enable_cloud_deploy10falseProvisions a Cloud Deploy pipeline.
cloud_deploy_stages10[dev, staging, prod(approval)]Ordered promotion stages. Each: name, target_name, service_name, require_approval, auto_promote.

7. Reliability & Scheduling

A. Scaling & Concurrency

min_instance_count defaults to 0 (scale-to-zero). Setting it to 1 keeps at least one backend instance running at all times, which matters for Penpot because:

  1. WebSocket connections from active collaborators are terminated on scale-to-zero events.
  2. JVM cold starts take 30–60 seconds, during which new connection attempts fail.
  3. Real-time collaboration requires a persistent event bus connection through Redis.

Redis handles WebSocket fan-out between backend replicas, so horizontal scaling is safe. All backend instances publish and subscribe to the same Redis channel, and design change events are broadcast to all connected clients regardless of which instance they are connected to.

B. Traffic Splitting

Traffic splitting is supported for canary deployments.

VariableGroupDefaultDescription
traffic_split4[]Percentage-based traffic allocation across named revisions. All entries must sum to 100. Empty sends 100% to the latest revision.

Penpot consideration: Canary deployments require careful handling of WebSocket connections. Active sessions connected to the old revision will remain there until reconnection. Design to accept both revisions being active simultaneously during the canary window.

C. Health Probes

Penpot 2.x does not expose an unauthenticated HTTP health endpoint on the root path. The module uses a TCP startup probe on port 6060 (checks that the backend process is listening), combined with startup_probe_config and health_check_config targeting /api/health for load balancer health checks.

VariableGroupDefaultDescription
startup_probe16{ type="TCP", port=6060, initial_delay_seconds=5, period_seconds=5, failure_threshold=40 }TCP startup probe. Allows up to 200 seconds total startup time (5s × 40).
liveness_probe16{ enabled=false }Disabled by default — Cloud Run does not support TCP liveness probes.
startup_probe_config16{ enabled=true, type="TCP", path="/api/health", timeout_seconds=240 }Alternative startup probe for load balancer health checks.
health_check_config16{ enabled=true, type="HTTP", path="/api/health", period_seconds=10 }HTTP liveness probe targeting /api/health.
uptime_check_config16{ enabled=false, path="/api/health" }Optional Cloud Monitoring uptime check. Alerts notify support_users.
alert_policies16[]Cloud Monitoring metric alert policies.

D. Auto Password Rotation

When enable_auto_password_rotation = true, a zero-downtime password rotation pipeline is provisioned:

  1. Secret Manager emits a rotation notification at every secret_rotation_period interval.
  2. Eventarc fires a Cloud Run rotation Job.
  3. The job generates a new password, updates the Cloud SQL PostgreSQL user, and writes a new secret version.
  4. After rotation_propagation_delay_sec seconds, the job restarts the Penpot backend.
VariableGroupDefaultDescription
enable_auto_password_rotation6falseEnables automated password rotation.
rotation_propagation_delay_sec690Seconds to wait after rotation before restarting services.

8. Integrations

A. Redis (Mandatory for Multi-Instance Deployments)

Redis is enabled by default (enable_redis = true). Penpot uses Redis as its WebSocket pub/sub event bus. When a designer makes a change, the backend publishes the event to Redis, and all other backend instances receive it and push it to their connected WebSocket clients. Without Redis, real-time collaboration only works when all clients are connected to the same backend instance — which is not guaranteed under Cloud Run's load balancing.

When enable_redis = true and redis_host is not provided, the module defaults to using the NFS server IP as the Redis host. For production deployments with multiple backend instances, point redis_host at a dedicated Google Cloud Memorystore for Redis instance.

VariableGroupDefaultDescription
enable_redis17trueEnables Redis for WebSocket pub/sub. Mandatory for multi-instance collaboration.
redis_host17""Redis server hostname or IP. Leave blank to use the NFS server IP. Use Memorystore for production.
redis_port17'6379'Redis server TCP port (string).
redis_auth17""Redis AUTH password. Sensitive — never stored in state.

B. Email (SMTP)

Penpot uses SMTP for team invitations and password resets. smtp_enabled must be set to true to activate email sending. Unlike other modules, SMTP is disabled by default in Penpot CloudRun — it must be explicitly enabled.

VariableGroupDefaultDescription
smtp_enabled7falseEnables SMTP email sending. Must be true for invitations and password resets to work.
smtp_from7""Default sender address. (e.g., 'noreply@example.com')
smtp_reply_to7""Default reply-to address.
smtp_host7""SMTP server hostname.
smtp_port7587SMTP port. 587 for STARTTLS, 465 for SSL.
smtp_username7""SMTP authentication username.
smtp_use_tls7trueEnable STARTTLS.
smtp_use_ssl7falseEnable SSL/TLS.

For the SMTP password, use secret_environment_variables:

secret_environment_variables = {
PENPOT_SMTP_PASSWORD = "penpot-smtp-password"
}

C. Backup Import & Recovery

When enable_backup_import = true, a dedicated Cloud Run Job restores an existing PostgreSQL backup into the provisioned Cloud SQL instance during the apply.

VariableGroupDefaultDescription
backup_schedule9'0 2 * * *'Cron expression (UTC) for automated daily backups.
backup_retention_days97Days to retain backup files in GCS.
enable_backup_import9falseTriggers a one-time restore on apply. Set false after a successful import.
backup_source9'gcs''gcs' (full GCS URI) or 'gdrive' (Drive file ID).
backup_uri9""Full GCS URI (e.g., 'gs://my-bucket/penpot-2024-01.sql') or Google Drive file ID.
backup_format9'sql'Backup file format. Options: sql, tar, gz, tgz, tar.gz, zip.

D. Custom SQL Scripts

VariableGroupDefaultDescription
enable_custom_sql_scripts11falseRuns SQL scripts from GCS after provisioning.
custom_sql_scripts_bucket11""GCS bucket containing SQL scripts.
custom_sql_scripts_path11""Path prefix within the bucket.
custom_sql_scripts_use_root11falseRun scripts as the root DB user.

E. Observability & Alerting

VariableGroupDefaultDescription
uptime_check_config16{ enabled=false, path="/api/health" }Optional Cloud Monitoring uptime check on /api/health.
alert_policies16[]Custom alert policies. Each: name, metric_type, comparison, threshold_value, duration_seconds.
support_users2[]Email addresses notified by uptime and alert policy triggers.

9. Exploring with the GCP Console

After a successful deployment, the following GCP Console paths are most useful for understanding and validating the Penpot deployment.

Cloud Run Services

Navigate to Cloud Run in the GCP Console. You will see multiple services:

  • The main Penpot backend service (port 6060) with its *.run.app URL.
  • The frontend service (nginx, port 80) whose URL is the user-facing entry point.
  • The exporter service (headless Chromium) for PDF/PNG/SVG export.

Click the backend service to view:

  • Revisions tab: active revision, traffic allocation, instance counts.
  • Logs tab: JVM startup messages, migration output, WebSocket connection events.
  • Metrics tab: request count, latency percentiles, instance count over time, container CPU/memory utilisation.

Cloud SQL

Navigate to SQL in the GCP Console. The Penpot PostgreSQL 15 instance shows:

  • Connection name (needed for gcloud sql connect).
  • Current connections and query insights.
  • Automated backup schedule and latest backup timestamp.

Click the instance name, then Databases to confirm the penpot database exists and Users to confirm the penpot user is present.

Cloud Storage

Navigate to Cloud Storage → Buckets. The penpot-assets bucket stores:

  • thumbnails/ — design file thumbnail images.
  • objects/ — uploaded design assets (images, fonts).

Verify the bucket exists and the Cloud Run SA has roles/storage.objectAdmin on it.

Secret Manager

Navigate to Security → Secret Manager. Secrets provisioned for this deployment include:

  • DB_PASSWORD (Penpot PostgreSQL application user password).

If SMTP is enabled, your SMTP secret will also appear here if added via secret_environment_variables.

Monitoring Uptime Checks

Navigate to Monitoring → Uptime checks. The Penpot uptime check polls /api/health from multiple global GCP regions. A failed check triggers an alert email to support_users.

Artifact Registry

Navigate to Artifact Registry. The Penpot backend, frontend, and exporter images are stored here after mirroring from Docker Hub. Check that all three images are present and tagged correctly.


10. Exploring with gcloud

The following gcloud commands are useful for inspecting and troubleshooting the Penpot Cloud Run deployment. Replace PROJECT_ID, REGION, and SERVICE_NAME with your actual values.

List all Penpot Cloud Run services in the project:

gcloud run services list \
--project=PROJECT_ID \
--region=REGION \
--filter="metadata.name:penpot" \
--format="table(metadata.name,status.url,status.conditions[0].status)"

Describe the Penpot backend service (shows env vars, scaling, traffic split):

gcloud run services describe penpot-backend \
--project=PROJECT_ID \
--region=REGION \
--format=yaml

Check the current revision and instance count:

gcloud run revisions list \
--project=PROJECT_ID \
--region=REGION \
--service=penpot-backend \
--format="table(metadata.name,status.observedGeneration,spec.containerConcurrency,status.conditions[0].status)"

Tail live logs from the Penpot backend:

gcloud run services logs tail penpot-backend \
--project=PROJECT_ID \
--region=REGION

Fetch recent logs filtered to errors only:

gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="penpot-backend" AND severity>=ERROR' \
--project=PROJECT_ID \
--limit=50 \
--format="table(timestamp,jsonPayload.message)"

Check the Cloud SQL instance status:

gcloud sql instances describe INSTANCE_NAME \
--project=PROJECT_ID \
--format="table(name,state,databaseVersion,settings.dataDiskSizeGb,ipAddresses[0].ipAddress)"

List Cloud SQL databases on the Penpot instance:

gcloud sql databases list \
--instance=INSTANCE_NAME \
--project=PROJECT_ID

Connect to the Penpot PostgreSQL database (requires Cloud SQL Auth Proxy or Cloud Shell):

gcloud sql connect INSTANCE_NAME \
--user=penpot \
--database=penpot \
--project=PROJECT_ID

List the penpot-assets GCS bucket contents by prefix:

gcloud storage ls gs://penpot-assets-PROJECT_ID/thumbnails/ --recursive

Check Secret Manager secrets for this deployment:

gcloud secrets list \
--project=PROJECT_ID \
--filter="name:penpot" \
--format="table(name,createTime,replication.automatic)"

Fetch the current database password (for diagnostic purposes only):

gcloud secrets versions access latest \
--secret=penpot-db-password \
--project=PROJECT_ID

Check Memorystore Redis instances (if using dedicated Redis):

gcloud redis instances list \
--region=REGION \
--project=PROJECT_ID \
--format="table(name,host,port,state,memorySizeGb,authEnabled)"

List Cloud Run Jobs (initialization or cron jobs):

gcloud run jobs list \
--project=PROJECT_ID \
--region=REGION \
--filter="metadata.name:penpot" \
--format="table(metadata.name,status.latestCreatedExecution.completionTimestamp,status.latestCreatedExecution.succeededCount)"

Check Artifact Registry for mirrored Penpot images:

gcloud artifacts docker images list \
REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY \
--filter="package:penpot" \
--format="table(package,tags,createTime,updateTime)"

Verify the Cloud Run SA has storage access to the assets bucket:

gcloud storage buckets get-iam-policy gs://penpot-assets-PROJECT_ID \
--format="table(bindings.role,bindings.members)"

11. Platform-Managed Behaviours

The following behaviours are applied automatically by Penpot CloudRun regardless of variable values.

BehaviourImplementationDetail
PostgreSQL 15 requireddatabase_type = "POSTGRES_15" defaultPenpot only supports PostgreSQL. Do not change to MySQL or SQL Server.
No db-init jobPenpot runs its own migrations at startupUnlike Django or Ghost, no db-init Cloud Run Job is injected. Penpot's Clojure process applies migrations automatically on first boot.
http1 protocol by defaultcontainer_protocol = "http1"Set "h2c" (HTTP/2 cleartext) to enable WebSocket multiplexing for real-time collaboration.
Scale-to-zero by defaultDefault min_instance_count = 0Cost-first default. Set 1 for active collaboration — WebSocket sessions are terminated on scale-down.
GCS assets bucketpenpot-assets bucket provisioned by Penpot CommonUsed for design asset storage. Accessed via PENPOT_STORAGE_BACKEND=gcs.
Image mirroring on by defaultenable_image_mirroring = true defaultAll three Penpot images are mirrored to Artifact Registry to avoid Docker Hub rate limits.
Redis for WebSocket fan-outenable_redis = true defaultWithout Redis, real-time collaboration breaks under load balancing across multiple instances.
Long request timeouttimeout_seconds = 3600 defaultPDF and PNG exports of complex multi-page designs can take minutes.
No auto-generated app secretsmodule_secret_env_vars = {}Penpot manages its own internal signing keys. No SECRET_KEY equivalent is created.

12. Variable Reference

All user-configurable variables, sorted by UI group then order. | Variable | Group | Default | Description | |---|---|---|---| | project_id | 1 | — | GCP project ID. Required. | | region | 1 | 'us-central1' | GCP deployment region. | | tenant_deployment_id | 2 | 'demo' | Short suffix appended to all resource names. | | support_users | 2 | [] | Email addresses for monitoring alerts. | | resource_labels | 2 | {} | Labels applied to all provisioned resources. | | application_name | 3 | 'penpot' | Base resource name. Do not change after initial deployment. | | display_name | 3 | 'Penpot - Open Source Design Tool' | Human-readable name. | | description | 3 | 'Penpot - Open-source design and prototyping tool for teams' | Service description. | | application_version | 3 | 'latest' | Penpot container image tag. | | deploy_application | 4 | true | Set false for infrastructure-only deployment. | | cpu_limit | 4 | '2000m' | CPU per backend instance. | | memory_limit | 4 | '2Gi' | Memory per backend instance. | | min_instance_count | 4 | 0 | Minimum backend instances. Raise to 1 for real-time collaboration. | | max_instance_count | 4 | 1 | Maximum backend instances. | | container_port | 4 | 6060 | Penpot backend port. Do not change. | | execution_environment | 4 | 'gen2' | Gen2 required for NFS mounts. | | timeout_seconds | 4 | 3600 | Max request duration. | | enable_cloudsql_volume | 4 | true | Connects via Cloud SQL Auth Proxy Unix socket. | | container_protocol | 4 | 'http1' | Set 'h2c' for HTTP/2 WebSocket multiplexing. | | enable_image_mirroring | 4 | true | Mirrors images into Artifact Registry. | | traffic_split | 4 | [] | Canary/blue-green traffic allocation. | | service_annotations | 4 | {} | Advanced Cloud Run annotations. | | service_labels | 4 | {} | Labels applied to the Cloud Run service. | | penpot_flags | 5 | 'enable-registration enable-login disable-demo-users' | Penpot feature flags. | | jvm_max_heap | 5 | '1g' | JVM maximum heap. ~50% of memory_limit. | | jvm_min_heap | 5 | '512m' | JVM initial heap. | | environment_variables | 6 | {} | Plain-text env vars. | | secret_environment_variables | 6 | {} | Secret Manager references. | | secret_propagation_delay | 6 | 30 | Seconds to wait after secret creation. | | secret_rotation_period | 6 | '2592000s' | Secret Manager rotation notification frequency. | | enable_auto_password_rotation | 6 | false | Automated zero-downtime password rotation. | | rotation_propagation_delay_sec | 6 | 90 | Seconds after rotation before restarting services. | | smtp_enabled | 7 | false | Enables SMTP email. Required for invitations and password resets. | | smtp_from | 7 | "" | Sender email address. | | smtp_reply_to | 7 | "" | Reply-to email address. | | smtp_host | 7 | "" | SMTP server hostname. | | smtp_port | 7 | 587 | SMTP port. | | smtp_username | 7 | "" | SMTP authentication username. | | smtp_use_tls | 7 | true | Enable STARTTLS. | | smtp_use_ssl | 7 | false | Enable SSL/TLS. | | ingress_settings | 8 | 'all' | 'all', 'internal', or 'internal-and-cloud-load-balancing'. | | vpc_egress_setting | 8 | 'PRIVATE_RANGES_ONLY' | VPC egress routing. | | enable_iap | 8 | false | Enables IAP on the Cloud Run service. | | iap_authorized_users | 8 | [] | Users/SAs granted IAP access. | | iap_authorized_groups | 8 | [] | Google Groups granted IAP access. | | backup_schedule | 9 | '0 2 * * *' | Cron expression for automated backups. | | backup_retention_days | 9 | 7 | Days to retain backup files. | | enable_backup_import | 9 | false | Triggers a one-time restore on apply. | | backup_source | 9 | 'gcs' | 'gcs' or 'gdrive'. | | backup_uri | 9 | "" | Full GCS URI or Google Drive file ID. | | backup_format | 9 | 'sql' | Backup format. | | enable_cicd_trigger | 10 | false | Provisions a Cloud Build GitHub trigger. | | github_repository_url | 10 | "" | Full HTTPS GitHub URL. | | github_token | 10 | "" | GitHub PAT. Sensitive. | | github_app_installation_id | 10 | "" | GitHub App installation ID. | | cicd_trigger_config | 10 | { branch_pattern = "^main$" } | Advanced Cloud Build trigger config. | | enable_cloud_deploy | 10 | false | Provisions a Cloud Deploy progressive delivery pipeline. | | cloud_deploy_stages | 10 | [dev, staging, prod(approval)] | Cloud Deploy promotion stages. | | enable_binary_authorization | 10 | false | Enforces image attestation. | | enable_custom_sql_scripts | 11 | false | Runs SQL scripts from GCS. | | custom_sql_scripts_bucket | 11 | "" | GCS bucket for SQL scripts. | | custom_sql_scripts_path | 11 | "" | Path prefix within the bucket. | | custom_sql_scripts_use_root | 11 | false | Run scripts as root DB user. | | create_cloud_storage | 12 | true | Set false to skip additional bucket creation. | | storage_buckets | 12 | [] | Additional GCS buckets to provision. | | enable_nfs | 12 | true | Provisions NFS shared storage. | | nfs_mount_path | 12 | '/mnt/nfs' | Container NFS mount path. | | nfs_instance_name | 12 | "" | Existing NFS VM name. | | nfs_instance_base_name | 12 | 'app-nfs' | Base name for inline NFS VM. | | gcs_volumes | 12 | [] | GCS Fuse volume mounts. | | manage_storage_kms_iam | 12 | false | CMEK KMS key for storage buckets. | | enable_artifact_registry_cmek | 12 | false | CMEK for Artifact Registry images. | | database_type | 13 | 'POSTGRES_15' | Cloud SQL engine. Do not change. | | db_name | 13 | 'penpot' | PostgreSQL database name. Immutable. | | db_user | 13 | 'penpot' | PostgreSQL application user. Immutable. | | database_password_length | 13 | 32 | Auto-generated password length. | | enable_cloud_armor | 14 | false | Provisions Global HTTPS LB + Cloud Armor WAF. | | admin_ip_ranges | 14 | [] | CIDR ranges exempted from WAF rules. | | application_domains | 14 | [] | Custom domains with Google-managed SSL. | | enable_cdn | 14 | false | Enables Cloud CDN on the HTTPS LB. | | max_images_to_retain | 14 | 7 | Maximum Artifact Registry images to keep. | | delete_untagged_images | 14 | true | Delete dangling images from Artifact Registry. | | image_retention_days | 14 | 30 | Days before images are eligible for deletion. | | initialization_jobs | 15 | [] | One-shot Cloud Run Jobs. Leave empty — Penpot handles its own migrations. | | cron_jobs | 15 | [] | Recurring scheduled Cloud Run Jobs. | | startup_probe | 16 | TCP probe, port 6060, 40 retries | TCP startup probe. | | liveness_probe | 16 | { enabled=false } | Disabled — Cloud Run does not support TCP liveness. | | startup_probe_config | 16 | { enabled=true, type="TCP", timeout_seconds=240 } | LB startup probe. | | health_check_config | 16 | { enabled=true, type="HTTP", path="/api/health" } | HTTP liveness probe. | | uptime_check_config | 16 | { enabled=false, path="/api/health" } | Optional Cloud Monitoring uptime check. | | alert_policies | 16 | [] | Cloud Monitoring metric alert policies. | | enable_redis | 17 | true | Redis for WebSocket pub/sub. Mandatory for multi-instance. | | redis_host | 17 | "" | Redis hostname/IP. Defaults to NFS server IP. | | redis_port | 17 | '6379' | Redis TCP port. | | redis_auth | 17 | "" | Redis AUTH password. Sensitive. | | enable_vpc_sc | 18 | false | VPC-SC perimeter enforcement. | | vpc_cidr_ranges | 18 | [] | VPC-SC CIDR ranges. | | vpc_sc_dry_run | 18 | true | Log violations without blocking. | | organization_id | 18 | "" | GCP Organization ID for VPC-SC. | | enable_audit_logging | 18 | false | Detailed Cloud Audit Logs. |


13. Outputs

OutputDescription
service_nameName of the Penpot backend Cloud Run service.
backend_urlPublic URL of the backend Cloud Run service.
frontend_urlPublic URL of the frontend (nginx) Cloud Run service — the URL users open.
exporter_urlPublic URL of the exporter Cloud Run service.
service_locationGCP region of the deployment.
project_idGCP project ID.
deployment_idDeployment ID suffix used in resource names.
database_instance_nameName of the Cloud SQL PostgreSQL 15 instance.
database_nameName of the Penpot application database.
database_userName of the Penpot application database user.
database_password_secretSecret Manager secret name for the database password.
storage_bucketsCreated GCS storage buckets (including penpot-assets).
container_imageContainer image used for the backend deployment.
cicd_enabledWhether the CI/CD pipeline is enabled.
github_repository_urlGitHub repository URL connected for CI/CD.

Configuration Pitfalls & Sensible Defaults

Risk levels: Critical (data loss, full outage, security breach) — High (service unavailable or significant degradation) — Medium (degraded function or increased cost) — Low (minor impact).

VariableSensible DefaultRiskConsequence of Incorrect Value
project_id(required)CriticalNo default — deployment fails immediately.
database_type"POSTGRES_15"CriticalPenpot only supports PostgreSQL. Any other value breaks the backend at startup.
container_protocol"h2c" for multiplayer (module default "http1")CriticalUnder "http1", WebSocket multiplexing is unavailable and real-time collaboration degrades. Designs can still be saved but multiplayer editing may be non-functional.
min_instance_count1 for active collaboration (module default 0)HighUnder scale-to-zero, active WebSocket sessions are terminated and all collaborators are disconnected. JVM cold starts cause 30–60 second reconnection delays.
redis_host"" (auto-resolves to NFS IP)HighIf enable_nfs = false and redis_host is also empty, the backend cannot connect to Redis and will fail to start multi-instance collaboration.
enable_redistrueHighDisabling Redis breaks real-time collaboration under any load balancing. Single-instance deployments survive without Redis but scaling becomes impossible.
jvm_max_heap"1g"HighMust be set below memory_limit. Setting jvm_max_heap = "2g" on a memory_limit = "2Gi" container leaves no headroom for the JVM's off-heap memory, causing OOMKill. A safe ratio is 50% of memory_limit.
memory_limit"2Gi"HighPenpot's JVM requires headroom for off-heap memory, NIO buffers, and metaspace. Reducing below 1Gi causes frequent OOMKill under real workloads.
db_name"penpot"CriticalImmutable after deployment. Changing causes Terraform to recreate the database, destroying all Penpot designs, files, and team data.
db_user"penpot"CriticalImmutable after deployment. Changing recreates the Cloud SQL user and breaks all database connections.
penpot_flags"enable-registration enable-login disable-demo-users"MediumLeaving enable-registration active on a public deployment allows anyone to create an account. Use disable-registration for closed-team deployments and invite users via email.
smtp_enabledfalseHighWith SMTP disabled, Penpot cannot send team invitations or password reset emails. New team members can only be added by an admin through direct account creation.
application_domains[]MediumWithout a custom domain, the PENPOT_PUBLIC_URI is set to the predicted *.run.app URL. If users access Penpot via a custom domain without updating PENPOT_PUBLIC_URI, invitation email links point to the wrong URL.
backup_retention_days7MediumInsufficient for active design teams. Increase to 30+ days. A week of designs and files can represent significant creative work.
enable_cloud_armorfalseMediumWithout Cloud Armor, the Penpot frontend and API are exposed directly to the internet. WAF rules protect against common web attacks on the design API.
timeout_seconds3600LowReducing below 300 seconds may cause large PDF/PNG export operations to time out mid-export, returning an error to the designer.

Destroying Resources

Known Deletion Issue: Serverless IPv4 Address Release

When destroying a Cloud Run deployment, you may encounter an error similar to:

Error: Error waiting for Subnetwork to be deleted: The following serverless IPv4 address(es) on subnet ... are still in use.

Cause: GCP holds serverless IPv4 addresses on the VPC subnet asynchronously after a Cloud Run service is deleted. These addresses are released approximately 20–30 minutes after the Cloud Run service is removed.

Resolution: Wait 20–30 minutes after the initial destroy attempt, then re-run the destroy command:

tofu destroy

The second run will succeed once GCP has released the reserved addresses.