Skip to main content

Deployment Guide

Octopus runs in production on Google Kubernetes Engine, in GCP project octopus-prod-maiolabs (europe-west1). It moved there from AWS ECS Fargate on 2026-08-20.

For local development, see the Installation Guide.

How a deploy happens​

Push to the staging branch. That is the trunk, despite the name — there is one live environment, and this deploys it.

The deploy-gcp-prod.yml workflow then:

  1. authenticates to GCP by Workload Identity Federation — no service-account keys
  2. builds five images and pushes them to Artifact Registry, tagged with the short commit SHA
  3. fetches cluster credentials and runs infra/gcp/scripts/deploy.sh with that tag
  4. waits on the rollout of every Deployment and StatefulSet

To roll out an image that already exists, run the workflow manually and pass image_tag. Nothing is rebuilt.

What gets deployed​

ComponentKindImage
octopusDeployment.../octopus/app
portal-backendDeployment.../octopus/portal-backend
portal-frontendDeployment.../octopus/portal-frontend
docsDeployment.../octopus/docs
websiteDeployment.../octopus/website
falkordbStatefulSetmirrored upstream image, pinned
redisStatefulSetmirrored upstream image, pinned

Registry prefix is europe-west1-docker.pkg.dev/octopus-prod-maiolabs/octopus. CI builds the five application images; the two datastore images are third-party and pinned in the manifests.

Deploying by hand​

./infra/gcp/scripts/deploy.sh <image-tag>
./infra/gcp/scripts/deploy.sh <image-tag> --dry-run

The script pins --context on every kubectl call, applies the namespace first, substitutes the image tag into the manifests, and refuses a tag that is not present in Artifact Registry — without that check the pods sit in ImagePullBackOff and it reads as a cluster fault.

Infrastructure​

Terraform lives in infra/gcp/terraform/, with environments/prod/ as the only runnable root.

./infra/gcp/scripts/tf.sh plan
./infra/gcp/scripts/tf.sh apply

Always use the wrapper. db_password has no default, so a bare terraform run leaves it unset and the plan quietly proposes changing the Cloud SQL password — an apply would rotate it and drop every live connection. The wrapper sources the real value from Secret Manager.

The GCS state backend needs Application Default Credentials. gcloud auth login is a different credential store and is not enough:

gcloud auth application-default login

Configuration and secrets​

Secrets live in Secret Manager as octopus-prod-<NAME>. Pods read them through Workload Identity — there are no keys or static credentials in the cluster.

Terraform creates the secret containers; values are loaded separately by infra/gcp/scripts/sync-secrets.sh. No secret value passes through Terraform state.

Networking and TLS​

A GKE Ingress provisions a Google Cloud Load Balancer with container-native NEG backends, on the static IP octopus-prod-ingress-ip. It routes three hostnames:

  • octopus.maiolabs.ai — the API and website
  • portal.octopus.maiolabs.ai — the portal
  • documentation.octopus.maiolabs.ai — these docs

Certificates come from Certificate Manager using DNS authorization, which matters: validation happens over a TXT record, so a certificate is issued and active before any traffic points at the load balancer. HTTP-validated certificates cannot do that, and would leave a gap at cutover.

Observability​

LogsCloud Logging. resource.type="k8s_container", namespace octopus
ErrorsLog-based metric octopus-prod-app-errors, labelled by container
Alerts5xx spike at the load balancer; pod crash loop. Both email
DatabaseCloud SQL query insights

Scaling​

  • Horizontal: raise replicas in the Deployment. The node pool autoscales 2→4.
  • Vertical: adjust the container resource requests, then re-apply.
  • Database: db_tier in terraform.tfvars. Currently db-g1-small, sized from 30 days of measured load on the AWS source (CPU averaged 3.4%).

Local Docker​

docker compose up -d

See the Installation Guide for the environment variables a local run needs.