Skip to main content

Google Cloud Platform Deployment

Use the GCP deployment when PhenoWorks needs managed infrastructure for the API, UI, background worker, metadata database, object storage, and queueing. The deployment in deployment/gcp/ uses Cloud Run, Cloud SQL, GCS, Pub/Sub, Artifact Registry, Cloud Build, and Terraform.

Current Versions

ComponentVersion or setting
PhenoWorks backend package1.0.0
PhenoWorks UI package1.0.0
API/worker Python imageghcr.io/astral-sh/uv:python3.11-bookworm
Terraform>= 1.6.0
Google Terraform provider~> 6.0
Cloud SQL database versionPOSTGRES_16
Default Cloud SQL editionENTERPRISE
Default Cloud SQL tierdb-custom-2-7680
TiTiler imageghcr.io/developmentseed/titiler:latest

Architecture

The GCP stack maps the local Compose services to managed services:

PhenoWorks responsibilityGCP service
APICloud Run service built from deployment/gcp/Dockerfile.api
UICloud Run service built from deployment/gcp/Dockerfile.ui
Celery workerCloud Run service using the API image and a worker command
Metadata databaseCloud SQL for PostgreSQL 16
File and upload storageGCS bucket
Celery brokerPub/Sub through Celery's gcpubsub:// transport
Container imagesArtifact Registry
InfrastructureTerraform under deployment/gcp/terraform

The GCP API image installs the cloud-gcp extra, so it includes the GCS and Pub/Sub dependencies needed by the hosted runtime.

Prerequisites

Install and authenticate:

  • gcloud
  • Terraform 1.6.0 or newer
  • Access to create Artifact Registry, Cloud Build, Cloud Run, Cloud SQL, GCS, Pub/Sub, Secret Manager, and IAM resources in the target project

Set the active project:

gcloud config set project <your-gcp-project-id>

Configure Terraform

From the repository root:

cd deployment/gcp/terraform
cp terraform.tfvars.example terraform.tfvars

Edit terraform.tfvars. At minimum, set:

project_id = "<your-gcp-project-id>"
database_password = "<database-password>"
secret_key = "<phenoworks-secret-key>"
admin_email = "admin@phenoworks.local"
admin_password = "<admin-password>"
nextauth_secret = "<auth-secret>"
image_tag = "latest"

Image names are centralized in Terraform:

region = "us-central1"
artifact_repository_id = "phenoworks"
api_image_name = "phenoworks-api"
ui_image_name = "phenoworks-ui"

Terraform derives deployed images in this shape:

<region>-docker.pkg.dev/<project_id>/<artifact_repository_id>/<api_image_name>:<image_tag>
<region>-docker.pkg.dev/<project_id>/<artifact_repository_id>/<ui_image_name>:<image_tag>

If an older terraform.tfvars file still has full api_image = ... or ui_image = ... entries, remove them and use the centralized image settings above.

Build and Deploy

Initialize Terraform:

terraform init

Create Artifact Registry first:

terraform apply -target=google_artifact_registry_repository.phenoworks

Build and push the API and UI images:

cd ../../..

gcloud builds submit \
--config deployment/gcp/cloudbuild.api.yaml \
--substitutions _REGION=us-central1,_REPOSITORY=phenoworks,_IMAGE_NAME=phenoworks-api,_TAG=latest \
.

gcloud builds submit \
--config deployment/gcp/cloudbuild.ui.yaml \
--substitutions _REGION=us-central1,_REPOSITORY=phenoworks,_IMAGE_NAME=phenoworks-ui,_TAG=latest \
.

Deploy the infrastructure and Cloud Run services:

cd deployment/gcp/terraform
terraform apply

After the first full apply, read the generated service URLs:

terraform output api_url
terraform output ui_url

Copy those outputs back into terraform.tfvars:

public_api_base_url = "https://<api-cloud-run-url>/api"
public_ui_origin = "https://<ui-cloud-run-url>"

Apply again:

terraform apply

The second apply lets the API generate public TiTiler COG URLs with the final API base URL and lets GCS resumable upload sessions accept the final UI origin.

Hosted Runtime Settings

The Terraform deployment configures the hosted stack with managed GCP backends:

PHENOWORKS_UPLOAD_STORE_URI=gs://<bucket>
PHENOWORKS_RESUMABLE_UPLOAD_BACKEND=gcs
PHENOWORKS_OPERATION_QUEUE_BACKEND=celery
PHENOWORKS_CELERY_BROKER_URL=gcpubsub://projects/<project-id>
PHENOWORKS_CELERY_PUBSUB_QUEUE_NAME_PREFIX=phenoworks-

The worker is a Cloud Run service with worker_min_instances = 1, always-allocated CPU, and Celery --concurrency=1 so one worker instance processes one heavy raster task at a time. The default worker sizing is worker_cpu = "4" and worker_memory = "16Gi".

Cloud SQL uses the Cloud Run Cloud SQL connector. The starter Terraform enables public IPv4 because Cloud SQL requires at least one connectivity mode. For private-only production deployments, replace that with private IP or PSC before setting cloud_sql_ipv4_enabled = false.

Updating Images

Rebuild images with an immutable tag:

TAG=$(git rev-parse --short HEAD)

gcloud builds submit \
--config deployment/gcp/cloudbuild.api.yaml \
--substitutions _REGION=us-central1,_REPOSITORY=phenoworks,_IMAGE_NAME=phenoworks-api,_TAG=$TAG \
.

gcloud builds submit \
--config deployment/gcp/cloudbuild.ui.yaml \
--substitutions _REGION=us-central1,_REPOSITORY=phenoworks,_IMAGE_NAME=phenoworks-ui,_TAG=$TAG \
.

Set the same tag in terraform.tfvars:

image_tag = "<tag>"

Then apply:

cd deployment/gcp/terraform
terraform apply

Outputs

After deployment, inspect:

terraform output api_url
terraform output ui_url
terraform output worker_url
terraform output titiler_url
terraform output storage_bucket
terraform output cloud_sql_connection_name
terraform output celery_pubsub_queue

Teardown

Preview the destroy plan:

cd deployment/gcp/terraform
terraform plan -destroy

By default, deletion_protection = true, so Terraform will not delete Cloud Run services, the Cloud SQL instance, or force-delete the GCS bucket. For a disposable environment, set:

deletion_protection = false

Apply that change, then destroy:

terraform apply
terraform destroy

Do not disable deletion protection for environments that contain production research data.