Skip to main content

AFFiNE on Google Cloud Run

AFFiNE on Google Cloud Run

AFFiNE is an open-source, privacy-first knowledge base that unifies docs, whiteboards, and databases in one workspace — a self-hostable alternative to Notion and Miro. This module deploys AFFiNE on Cloud Run v2 on top of the App_CloudRun foundation, which provisions and manages the shared Google Cloud infrastructure.

This guide focuses on the cloud services AFFiNE uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics common to every Cloud Run application — service identity, ingress and load balancing, scaling and concurrency, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC Service Controls, backups, and the deployment lifecycle — refer to the App_CloudRun foundation guide rather than repeating them here.


1. Overview

AFFiNE's self-host server runs as a single Node.js container on Cloud Run v2. The deployment wires together a focused set of Google Cloud services:

CapabilityGoogle Cloud serviceNotes
ComputeCloud Run v2Node.js service, 2 vCPU / 4 GiB by default, single always-on instance
DatabaseCloud SQL for PostgreSQL 15Required — AFFiNE does not support MySQL (enforced at plan time)
Real-time collaborationRedisMandatory — Yjs document-sync pub/sub and the job queue; the NFS host co-hosts the default Redis
Blob storageFilestore / NFSUploaded attachments persisted at /root/.affine/storage (gen2 required)
Object storageCloud StorageA dedicated storage bucket provisioned automatically
SecretsSecret ManagerDatabase password managed automatically; AFFiNE needs no app secret
Container imageCloud Build + Artifact RegistryThin custom build over ghcr.io/toeverything/affine
IngressCloud Run URL / Cloud Load BalancingDefault run.app URL, optional external HTTPS LB + custom domain

Sensible defaults worth knowing up front:

  • PostgreSQL 15 is mandatory. A plan-time validation restricts database_type to PostgreSQL versions; MySQL is rejected.
  • Redis is mandatory. A plan-time validation fails the deploy if enable_redis = false. With no explicit redis_host, the NFS server IP is used as the Redis endpoint.
  • Single instance by design. min_instance_count = 1, max_instance_count = 1, cpu_always_allocated = true — real-time collaboration WebSockets must stay reachable and CPU-fed, and per-process collab state plus filesystem blobs make horizontal scaling unsafe.
  • No Cloud SQL socket. enable_cloudsql_volume = false: AFFiNE consumes a URL-authority DATABASE_URL that cannot carry the socket path's colons, so the entrypoint connects over the instance private IP with sslmode=require.
  • Two init jobs on apply. db-init idempotently creates the database and user; affine-migrate runs AFFiNE's self-host-predeploy (schema migration + signing-key generation) before the server starts.
  • No application secret. AFFiNE persists its own signing key in PostgreSQL during migration; only the auto-generated database password lives in Secret Manager.
  • Health probes target / — AFFiNE returns HTTP 200 on its root path once ready.
  • application_version = "latest" maps to stable — AFFiNE publishes no latest image tag.
  • Full-text/vector search is disabled (AFFINE_INDEXER_ENABLED = "false") — the indexer needs a vector backend not provisioned here.

2. Google Cloud Services & How to Explore Them

All commands assume PROJECT and REGION are set. Service and resource names are reported in the deployment Outputs.

A. Cloud Run — the AFFiNE service

AFFiNE runs as a Cloud Run v2 service pinned to a single always-on instance. Each deployment creates an immutable revision; traffic moves to the newest healthy one.

  • Console: Cloud Run → select the service for revisions, traffic, logs, and metrics.
  • CLI:
    gcloud run services list --project "$PROJECT" --region "$REGION"
    gcloud run services describe <service-name> --project "$PROJECT" --region "$REGION"
    gcloud run revisions list --service <service-name> --project "$PROJECT" --region "$REGION"

See App_CloudRun for scaling, concurrency, execution environment, and traffic splitting.

B. Cloud SQL for PostgreSQL 15

AFFiNE stores workspaces, documents, users, and its own signing key in a managed Cloud SQL for PostgreSQL 15 instance. Because AFFiNE's DATABASE_URL is a URL-authority DSN, the service connects over the private IP with TLS (sslmode=require) rather than the Auth Proxy socket. On first deploy the db-init job creates the application database and user, then affine-migrate creates the schema.

  • Console: SQL → select the instance for connections, backups, flags, metrics.
  • CLI:
    gcloud sql instances list --project "$PROJECT"
    gcloud sql instances describe <instance-name> --project "$PROJECT"
    gcloud sql connect <instance-name> --user=<db-user> --project "$PROJECT"

The instance name, database, user, and password secret are in the Outputs. See App_CloudRun for the connection model, backups, and password rotation.

C. Redis — real-time collaboration

Redis carries AFFiNE's Yjs document-sync pub/sub and its background job queue — the deployment refuses to plan without it. When no external redis_host is configured, the shared NFS server's co-hosted Redis is used automatically.

  • Console: Memorystore → Redis (if using a managed instance); Compute Engine → VM instances (the NFS/Redis host).
  • CLI:
    redis-cli -h <redis-host> ping
    redis-cli -h <redis-host> info clients

D. Filestore (NFS) and Cloud Storage

Uploaded blobs (images, attachments, file embeds) are written to an NFS share mounted at /root/.affine/storage, so they survive revisions and restarts. A dedicated Cloud Storage bucket (suffix storage) is also provisioned automatically. The gen2 execution environment is required for NFS mounts.

  • Console: Filestore → Instances (or Compute Engine → VM instances for the self-managed NFS VM); Cloud Storage → Buckets.
  • CLI:
    gcloud filestore instances list --project "$PROJECT"
    gcloud storage buckets list --project "$PROJECT"
    gcloud storage ls gs://<storage-bucket>/ # bucket name is in the Outputs

See App_CloudRun for the NFS mount, GCS Fuse, and CMEK.

E. Secret Manager

The auto-generated database password is the only secret — AFFiNE generates and stores its signing key inside PostgreSQL during the affine-migrate job, so no application secret exists to manage or rotate.

  • Console: Security → Secret Manager.
  • CLI:
    gcloud secrets list --project "$PROJECT"
    gcloud secrets versions access latest --secret=<secret-name> --project "$PROJECT"

See App_CloudRun for injection and rotation details.

F. Networking & ingress

The service is reachable at its run.app URL by default. An external HTTPS load balancer with a custom domain, Cloud CDN, and Cloud Armor can be layered on; ingress settings and VPC egress control connectivity. The cloud entrypoint defaults AFFINE_SERVER_EXTERNAL_URL to the injected service URL so invites and share links resolve correctly.

  • Console: Cloud Run (service URL); Network services → Load balancing.
  • CLI:
    gcloud run services describe <service-name> --region "$REGION" --format='value(status.url)'
    gcloud compute addresses list --project "$PROJECT"

See App_CloudRun.

G. Cloud Logging & Monitoring

Container logs flow to Cloud Logging; Cloud Run and Cloud SQL metrics flow to Cloud Monitoring, with optional uptime checks and alert policies.

  • Console: Logging → Logs Explorer; Monitoring → Dashboards / Alerting.
  • CLI:
    gcloud run services logs read <service-name> --project "$PROJECT" --region "$REGION" --limit 50

3. AFFiNE Application Behaviour

  • Two-stage database setup. On apply, db-init (image postgres:15-alpine) idempotently creates the AFFiNE role and database, grants privileges, and best-effort grants cloudsqlsuperuser so migrations can CREATE EXTENSION. Then affine-migrate runs AFFiNE's node ./scripts/self-host-predeploy using the built app image — idempotent schema migration plus signing-key generation. Both are safe to re-run; affine-migrate retries up to 3 times.
  • Signing key lives in the database. Unlike most apps there is no APP_SECRET-style env var: the key generated by self-host-predeploy is persisted in PostgreSQL, so the deployment carries no application secret to desync or rotate.
  • DSN assembly at startup. The cloud entrypoint builds DATABASE_URL from the Foundation-injected DB_* vars (URL-encoding the credentials) and maps REDIS_HOST/PORT/AUTH to AFFiNE's REDIS_SERVER_*. On Cloud Run it connects to the Cloud SQL private IP with sslmode=require; a preset DATABASE_URL env var takes precedence.
  • External URL. AFFINE_SERVER_EXTERNAL_URL defaults to the Cloud Run service URL. Set it explicitly (via environment_variables) once a custom domain is live so share links and invite emails use the right host.
  • First-run setup. Open the service URL and create the first account — on a fresh AFFiNE self-host instance the first registered user becomes the server administrator, and the admin panel is at <url>/admin.
  • Health path. Startup, liveness, and readiness probes target /, which returns HTTP 200 once the server is ready (startup window: 60 s initial delay + up to 30 × 15 s).
  • Scaling constraint. The service is pinned to exactly one always-on instance. Blobs on the NFS filesystem and per-process Yjs state make multiple instances unsafe; scale vertically (cpu_limit / memory_limit) instead.
  • Verification:
    SERVICE=$(gcloud run services list --project "$PROJECT" --region "$REGION" \
    --filter="metadata.name~affine" --format="value(metadata.name)" --limit=1)
    SERVICE_URL=$(gcloud run services describe "$SERVICE" --project "$PROJECT" \
    --region "$REGION" --format="value(status.url)")
    curl -s -o /dev/null -w "%{http_code}\n" "$SERVICE_URL/" # expect 200

4. Configuration Variables

Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for AFFiNE are listed; every other input is inherited from App_CloudRun with its standard behaviour.

Group 1 — Project & Identity

VariableDefaultDescription
project_id(required)Target Google Cloud project.
regionus-central1Region for the service and regional resources.

Group 2 — Deployment Environment

VariableDefaultDescription
tenant_deployment_iddemoShort suffix that makes resource names unique per environment.
support_users[]Emails granted IAM access and monitoring alerts.

All other inputs follow standard App_CloudRun behaviour.

Group 3 — Application Identity

VariableDefaultDescription
application_nameaffineBase name for resources. Do not change after first deploy.
application_versionstableImage tag for ghcr.io/toeverything/affine; latest maps to stable. Increment to trigger a rebuild and new revision.

All other inputs follow standard App_CloudRun behaviour.

Group 4 — Runtime & Scaling

VariableDefaultDescription
cpu_limit / memory_limit2000m / 4GiAFFiNE needs at least 2Gi for reliable operation.
container_port3010AFFiNE's native self-host server port.
min_instance_count1Keeps the collaboration WebSocket server always reachable — scale-to-zero drops live editing sessions.
max_instance_count1Pinned. Blobs on the filesystem + per-process collab state make horizontal scaling unsafe.
cpu_always_allocatedtrueWebSocket Yjs sync is starved under request-based CPU throttling — keep true for a live collaborative editor.
enable_cloudsql_volumefalseAFFiNE's URL-authority DATABASE_URL cannot carry the Cloud SQL socket path; the entrypoint uses the private IP with sslmode=require.
container_image_sourcecustomThe thin-wrapper build supplies the cloud entrypoint — required.
execution_environmentgen2Required for the NFS mount.

All other inputs follow standard App_CloudRun behaviour.

Group 6 — Environment Variables & Secrets

VariableDefaultDescription
environment_variables{}Merged over the Affine_Common defaults (NODE_ENV, AFFINE_SERVER_HOST/PORT, AFFINE_CONFIG_PATH, AFFINE_INDEXER_ENABLED=false). Never set PORT — it is a Cloud Run reserved name and breaks Job creation.
secret_environment_variables{}AFFiNE needs no application secret by default.

All other inputs follow standard App_CloudRun behaviour.

Group 11 — Storage & Filesystem

VariableDefaultDescription
enable_nfstrueBlob storage and the default Redis host. Keep true unless an external redis_host is supplied.
nfs_mount_path/root/.affine/storageWhere AFFiNE persists uploaded blobs.

All other inputs follow standard App_CloudRun behaviour. The storage GCS bucket is always provisioned by Affine_Common.

Group 12 — Database Backend

VariableDefaultDescription
database_typePOSTGRES_15AFFiNE requires PostgreSQL — MySQL is rejected at plan time.
db_name / db_useraffine / affineImmutable after first deploy.

All other inputs follow standard App_CloudRun behaviour.

Group 13 — Jobs & Scheduled Tasks

VariableDefaultDescription
initialization_jobs[]Leave empty for the built-in db-init (postgres:15-alpine) + affine-migrate (built app image) jobs.

All other inputs follow standard App_CloudRun behaviour.

Group 14 — Observability & Health

VariableDefaultDescription
startup_probeHTTP /, 60 s delay, 30 failuresGenerous first-boot window.
liveness_probeHTTP /, 60 s delayRoot path returns 200 once ready.
uptime_check_configdisabled, path /Cloud Monitoring uptime check.

All other inputs follow standard App_CloudRun behaviour.

Group 21 — Redis

VariableDefaultDescription
enable_redistrueMandatory — plan-time validation rejects false. Yjs pub/sub + job queue.
redis_host""Leave empty to use the NFS host IP.

All other inputs follow standard App_CloudRun behaviour.

Group 22 — VPC Service Controls

All inputs follow standard App_CloudRun behaviour (enable_vpc_sc, vpc_cidr_ranges, vpc_sc_dry_run, organization_id, enable_audit_logging).


5. Outputs

Returned on a successful deployment — the quickest way to locate and explore the running resources.

OutputDescription
service_nameCloud Run service name.
service_urlDefault run.app URL of the service.
service_locationRegion the service runs in.
stage_servicesStage-specific service URLs (Cloud Deploy).
load_balancer_ip / load_balancer_urlExternal HTTPS load balancer IP / URL (when enabled).
database_instance_nameCloud SQL instance name.
database_name / database_userApplication database name / user.
database_password_secretSecret Manager secret holding the DB password.
database_host / database_portDB endpoint / port (host is sensitive).
storage_bucketsCreated Cloud Storage buckets (includes the AFFiNE storage bucket).
network_name / network_exists / regionsVPC network, presence, regions.
container_image / container_registryDeployed image and Artifact Registry repo.
monitoring_enabled / monitoring_notification_channels / uptime_check_namesMonitoring status, channels, uptime checks.
initialization_jobsNames of the setup jobs (db-init, affine-migrate).
deployment_id / tenant_id / resource_prefixNaming identifiers.
project_id / project_numberProject identifiers.
cicd_enabled / github_repository_url / github_repository_owner / github_repository_name / cicd_configurationCI/CD status and details.
artifact_registry_repository / cloudbuild_trigger_name / cloudbuild_trigger_idRegistry and build trigger.
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_modeVPC-SC status.
audit_logging_enabled / artifact_registry_cmek_enabledAudit logging and CMEK status.

6. Configuration Pitfalls & Sensible Defaults

Cross-variable validation runs at plan time (validation.tf): it enforces PostgreSQL, mandatory Redis with a resolvable host, min ≤ max instance counts, and rejects a Cloud SQL volume with database_type = "NONE" — misconfigurations fail fast instead of producing a broken deployment.

Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).

SettingSensible valueRiskConsequence if wrong
database_typePOSTGRES_15CriticalAFFiNE requires PostgreSQL; MySQL is rejected at plan time.
db_name / db_userset onceCriticalImmutable after first deploy; renaming recreates the DB/user and destroys all workspaces.
enable_redistrueCriticalMandatory — real-time collaboration and the job queue need Redis; false fails the plan.
redis_host"" (NFS) or explicitCriticalRedis on with NFS off and no host set fails validation; a wrong host breaks doc sync at runtime.
enable_nfstrueCriticalWithout NFS, uploaded blobs land on ephemeral disk and vanish on every revision/restart — and the default Redis host disappears.
container_port3010CriticalAFFiNE's native port; a mismatch fails every health probe.
max_instance_count1CriticalMore than one instance splits per-process collab state and filesystem blobs — silent data divergence.
container_image_sourcecustomHighThe upstream image lacks the entrypoint that assembles DATABASE_URL / REDIS_SERVER_* — the server cannot reach its database.
enable_cloudsql_volumefalseHighThe socket path's colons break AFFiNE's URL parser (invalid port); keep private-IP + sslmode=require.
cpu_always_allocatedtrueHighRequest-based throttling starves the Yjs WebSocket sync between requests — live editing stalls.
min_instance_count1HighScale-to-zero drops active collaboration sessions and adds cold-start delays.
memory_limit4Gi (≥ 2Gi)HighNode.js OOM during document sync or migration below 2Gi.
environment_variables PORTnever setHighPORT is Cloud Run-reserved; setting it makes every Job creation fail with HTTP 400.
application_versionstable (pinned tag)MediumNonexistent tags (e.g. literal latest) fail the image build; the module maps lateststable.
execution_environmentgen2HighNFS mounts require gen2.
AFFINE_SERVER_EXTERNAL_URLservice URL / custom domainMediumWrong host breaks invite links and share URLs.

For the foundation behaviour referenced throughout — service identity, scaling and concurrency, ingress and load balancing, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, backups, and image mirroring — see App_CloudRun. AFFiNE-specific application configuration shared with the GKE variant is described in Affine_Common.