Skip to content

Deploy to GCP

Choose this target when you want a managed cloud deployment on Google Cloud instead of a self-managed single host.

This page is the high-level entry point for GCP deployments. For the deeper Cloud Run runbook, see Google Cloud Deployment.

When to choose this target

Choose GCP if you want:

  • managed application hosting on Cloud Run
  • managed PostgreSQL with Cloud SQL
  • Secret Manager, Artifact Registry, and Cloud Scheduler integration
  • a cleaner fit for teams already standardised on Google Cloud

Choose Deploy with Docker Compose instead if you want the simplest self-hosted production path on infrastructure you control directly.

What this target runs

The GCP deployment uses:

  • Cloud Run for the web service
  • Cloud Run for the worker service
  • Cloud SQL for PostgreSQL
  • Cloud Storage for file storage
  • Secret Manager for runtime configuration
  • Artifact Registry for container images
  • Cloud Scheduler for recurring jobs

Advanced validators are deployed separately from the main web and worker services.

Environment model

The GCP setup is designed around three stages:

Stage Purpose Typical use
dev development testing deploy new changes first
staging pre-production verification optional but useful for larger changes
prod production customer-facing environment

Each stage gets its own Cloud Run services, Cloud SQL instance, secrets, and queueing resources.

Signed credentials on GCP

GCP deployments should use Google Cloud KMS rather than a local PEM file. Set the credential-signing key in your stage .django env file:

GCP_KMS_SIGNING_KEY=projects/your-project/locations/your-region/keyRings/your-app-name-keys/cryptoKeys/credential-signing
CREDENTIAL_ISSUER_URL=https://validibot.example.com

The Cloud Run service account also needs permission to sign with that key. At minimum, grant the runtime service account:

  • roles/cloudkms.viewer
  • roles/cloudkms.signerVerifier

Use a different KMS key per stage so dev, staging, and prod credentials do not share the same issuer key material.

Set up the env files

Before any just gcp ... recipe will work, copy the env templates and fill in the values:

mkdir -p .envs/.production/.google-cloud
cp .envs.example/.production/.google-cloud/.just    .envs/.production/.google-cloud/.just
cp .envs.example/.production/.google-cloud/.django  .envs/.production/.google-cloud/.django
cp .envs.example/.production/.google-cloud/.build   .envs/.production/.google-cloud/.build

If you plan to deploy MCP as well, copy the MCP template:

cp .envs.example/.production/.google-cloud/.mcp     .envs/.production/.google-cloud/.mcp

Then edit the new files. The .just file holds deployment-time configuration (GCP project, region, app name) and is sourced into your shell — it never leaves your machine. The .django file holds runtime configuration and is uploaded to Secret Manager. The .build file holds build-time knobs (commercial-package selection, ENABLE_MCP_SERVER).

Typical first-time flow

Most first-time GCP setups follow this order:

source .envs/.production/.google-cloud/.just

just gcp init-stage dev
just gcp secrets dev
just gcp deploy-all dev
just gcp setup-data dev
just gcp validators-deploy-all dev
just gcp scheduler-setup dev

just gcp deploy-all runs migrations as part of its dependency chain, so there is no separate just gcp migrate dev step here. You can still run it explicitly if you need to (or set GCP_SKIP_MIGRATE=1 to skip it).

After that, verify the environment, then repeat the same process for staging or prod as needed.

Secrets checklist

Before just gcp secrets dev, make sure .envs/.production/.google-cloud/.django defines:

  • DJANGO_SECRET_KEY — Django session / signed-cookie key.
  • DJANGO_MFA_ENCRYPTION_KEY — Fernet key for MFA secret material. The app refuses to start without this, and the startup check validates the format (not just presence). Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
  • DATABASE_URL, POSTGRES_* — Cloud SQL connection.
  • MFA_TOTP_ISSUER — authenticator-app label (e.g. "Validibot Cloud").
  • STORAGE_BUCKET — media / submission bucket, printed at the end of init-stage.

Commercial add-ons may introduce additional env vars (for example, a GCS audit-archive bucket with CMEK encryption). Each add-on's own deployment docs lists the env vars it expects — a community GCP deployment uses the null / filesystem audit-archive backends and needs nothing beyond the list above.

Provisioned resources

just gcp init-stage {stage} is idempotent and creates, among other things:

  • Runtime and validator service accounts with IAM bindings.
  • Cloud SQL instance and database.
  • Cloud Tasks queue and Cloud Scheduler-ready KMS permissions.
  • Media/submissions GCS bucket ({app}-storage[-stage]) with public/private prefix IAM.
  • Secret Manager placeholder for django-env[-stage].

A community-only deployment uses the NullArchiveBackend for audit log retention, which needs no extra GCP resources. Deployments that layer on a commercial add-on with the GCS audit-archive backend provision the bucket, CMEK key, and IAM separately — see the add-on's own deployment docs.

See configure-mfa.md for key-generation and rotation procedures. The encryption key is stored in Secret Manager via just gcp secrets, never committed.

Cache table

Production uses Django's DatabaseCache backend by default (rather than Memorystore/Redis) — a zero-marginal-cost option that reuses the Cloud SQL instance for allauth rate limiting and TOTP replay protection. The just gcp migrate step runs createcachetable automatically on every deploy (idempotent — no-op after the first run). If you ever need higher cache throughput, set REDIS_URL to a Memorystore instance and the settings module switches backends automatically — see configure-mfa.md for the full upgrade path.

Routine deployment flow

For normal updates:

source .envs/.production/.google-cloud/.just

just gcp deploy-all dev

deploy-all runs migrations as part of its dependency chain, so a separate migrate step is not needed for a routine deploy. Promote to production only after the lower stage looks healthy.

Include the MCP server

The standalone FastMCP container exposes validation workflows to AI agents over the Model Context Protocol. On GCP it runs as its own Cloud Run service (validibot-mcp in prod, validibot-mcp-<stage> otherwise) with its own Artifact Registry image and service account, deployed independently from the main Django web service.

Source and image. The MCP code lives in this repo at mcp/ and is built from compose/production/mcp/Dockerfile. The image is a lightweight Python container (~80 MB) with FastMCP, httpx, and pydantic-settings only — no Django, no database drivers.

License gate. At startup the MCP server calls GET /api/v1/license/features/ against the Django API and refuses to serve traffic unless mcp_server is advertised. This only happens when validibot-pro (or enterprise) is installed. So a community-only deployment can build and deploy the image but the container will exit during the license check.

Configure the knobs

The MCP deploy tooling reads its public Cloud Run config from .envs/.production/.google-cloud/.build:

# Include the MCP container in ``just gcp deploy-all`` and unlock
# the ``just gcp mcp ...`` recipes. Requires validibot-pro to be
# installed so the runtime license check passes.
ENABLE_MCP_SERVER=true

# Public URL of YOUR Validibot Django API — the MCP server proxies
# tool calls here. There is no default; setting this wrong could
# accidentally proxy your users' traffic to another operator's API.
VALIDIBOT_MCP_API_BASE_URL=https://app.your-domain.example

# Optional anonymous x402 public config. Leave disabled unless the
# Django side also has matching X402_PAY_TO_ADDRESS and
# X402_ALLOWED_NETWORK_ASSET_PAIRS values.
VALIDIBOT_X402_ENABLED=false
VALIDIBOT_X402_TEST_MODE=false
VALIDIBOT_X402_NETWORK=eip155:8453
VALIDIBOT_X402_ASSET=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
VALIDIBOT_X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402

See .envs.example/.production/.google-cloud/.build for the full documented template.

Configure MCP auth

MCP has two independent auth chains, both of which need their own settings in .envs/.production/.google-cloud/.django:

1. End user → MCP server (OAuth 2.1). When an OAuth-capable MCP client (Claude Desktop, Cursor, Windsurf, Continue, Zed, etc.) connects, the MCP server proxies a Dynamic Client Registration flow to Django's OIDC provider. Required settings:

# Signing key for JWT access tokens (base64-encoded PEM). Generate
# once and back up securely — rotating invalidates every live session.
IDP_OIDC_PRIVATE_KEY_B64=<base64 of a fresh openssl genrsa 2048 -out key.pem>

# Shared secret for the confidential OAuth client the MCP server
# registers as. Must equal VALIDIBOT_OAUTH_CLIENT_SECRET in the
# .mcp file (openssl rand -hex 32).
IDP_OIDC_MCP_SERVER_CLIENT_SECRET=<hex random secret>

# Public URL of your MCP server. Drives both Django's OIDC audience
# claim and the confidential client's registered redirect URI.
VALIDIBOT_MCP_BASE_URL=https://mcp.your-domain.example

2. MCP server → Django API (Cloud Run OIDC identity token). Every tool call reaches Django via /api/v1/mcp/*, which requires a Google- signed identity token minted by the MCP service account. Required settings:

# Audience that Cloud Run stamps on identity tokens. Convention is
# the public URL of the service being called.
MCP_OIDC_AUDIENCE=https://app.your-domain.example

# Allowlist of service-account emails permitted to mint tokens for
# the audience above. Must include the SA provisioned by
# ``just gcp mcp setup prod``.
MCP_OIDC_ALLOWED_SERVICE_ACCOUNTS=validibot-mcp-prod@your-project.iam.gserviceaccount.com

Django refuses to boot if MCP_OIDC_AUDIENCE is set but the allowlist is empty — a safety guard against accepting tokens from any Google SA that can mint to the audience.

See .envs.example/.production/.google-cloud/.django for the fully commented template.

Deploy

First-time setup provisions the MCP service account, IAM bindings, and Artifact Registry access:

source .envs/.production/.google-cloud/.just
just gcp mcp setup prod

Then upload the MCP secret (OAuth client credentials, etc.) and deploy the service. You have three levels of granularity:

# Umbrella — pushes every secret that might have changed
just gcp secrets prod
# Equivalent to: gcp django secrets + gcp mcp secrets

# Surgical — just one service
just gcp django secrets prod   # only .django → django-env
just gcp mcp secrets prod      # only .mcp → mcp-env
# Full deploy — Django web + worker + scheduler + MCP build + MCP deploy
just gcp deploy-all prod

# MCP-only deploy — useful for hotfixing just the MCP image
just gcp mcp build
just gcp mcp deploy prod

Routing

To expose MCP on a custom domain via the load balancer you set up for Django, run:

just gcp mcp lb-add prod mcp.your-domain.example

That provisions a serverless NEG, a backend service, adds the MCP hostname to the managed SSL certificate, and locks the Cloud Run service's ingress to load-balancer-only.

Domain and networking

There are two normal ways to expose a GCP deployment publicly:

  • Cloud Run domain mappings for the simpler path in supported regions
  • a global HTTP(S) load balancer for the more production-oriented path

If you need a custom domain, SSL, or a single public entrypoint, see the domain section in Google Cloud Deployment.

Good fits for this target

GCP is a good fit when:

  • you already use Google Cloud
  • you want managed infrastructure rather than running a VM yourself
  • you need a cleaner path to multi-environment deployments

Use these guides after choosing GCP: