Deploy on GCP
Run PandaStack on Google Cloud with Terraform — a single nested-virt dev host, or the multi-node edge + agent topology.
GCP is the practical target for self-hosting because Compute Engine exposes nested virtualization on standard machine types, so Firecracker gets /dev/kvm without bare metal. The Terraform modules set enable_nested_virtualization = true on every host that runs sandboxes.
There is no deploy-gcp.sh. Two entry points exist in the repo:
| Path | Entry point | Terraform env |
|---|---|---|
| Single dev host (agent + API + dashboard + Caddy on one VM) | make tf-gcp-* | infra/terraform/envs/dev-gcp |
| Multi-node (edge MIG + agent MIG behind a global HTTPS LB) | deploy/deploy-gcp-multi.sh | infra/terraform/envs/dev-gcp-multi |
Prereqs
gcloudCLI logged in (gcloud auth login) against a project with billing enabledterraform- A Cloudflare API token with
Zone:DNS:Edit, plus the zone ID — DNS records are managed by Terraform, not by a cloud DNS service - An SSH public key and the CIDR you will SSH from
The multi-node script additionally requires jq and curl, and checks for all four up front.
Single dev host
Copy the example variables file and fill it in:
cd infra/terraform/envs/dev-gcp
cp terraform.tfvars.example terraform.tfvarsgcp_project = "pandastack-production"
ssh_pubkey = "ssh-ed25519 AAAA... you@host"
ssh_allowed_cidr = "1.2.3.4/32"
cloudflare_api_token = "<token>"
cloudflare_zone_id = "<zone-id>"Then apply from the repo root:
make tf-gcp-init
make tf-gcp-plan
make tf-gcp-apply
make tf-gcp-outputThe targets run terraform -chdir=infra/terraform/envs/dev-gcp with -var-file=terraform.tfvars; apply and destroy pass -auto-approve.
This builds three modules plus DNS:
gcp-network— VPC, subnet, firewall scoped tossh_allowed_cidr, static external IPgcp-storage— a GCS bucket for kernels, templates, and snapshotsgcp-compute— one Compute Engine instance with nested virtualization enableddns— Cloudflare A records for the dashboard, API, andwwwsubdomains (setsubdomain_www = ""to skip the last one)
Outputs are instance_name, external_ip, app_fqdn, api_fqdn, and gcs_bucket_name.
Defaults worth changing
| Variable | Default | Notes |
|---|---|---|
machine_type | c2-standard-16 | Must be a family that supports nested virtualization |
boot_disk_size_gb | 100 | Templates and snapshots live on this disk |
boot_disk_type | pd-ssd | |
use_preemptible | true | Spot provisioning — the VM can be stopped by GCP |
gcp_region / gcp_zone | us-central1 / us-central1-a | |
cloudflare_zone_name | pandastack.ai | Set this to your own zone |
subdomain_app / subdomain_api | dev / api-dev |
use_preemptible defaults to true. A Spot VM can be reclaimed at any time, which takes every sandbox on that host with it. Set use_preemptible = false for anything you care about.
Tear it down with make tf-gcp-destroy.
Multi-node
deploy/deploy-gcp-multi.sh drives the dev-gcp-multi environment. It reads .env.local from the repo root and fails immediately if that file is missing, so populate DATABASE_URL, CLOUDFLARE_API_TOKEN, and CLOUDFLARE_ZONE_ID before running anything.
./deploy/deploy-gcp-multi.sh upup cross-builds pandastack-agent, pandastack-init, pandastack-daemon, and pandastack-api for linux/amd64, publishes them to a GCS build bucket, writes terraform.tfvars, runs terraform init -upgrade and terraform apply -auto-approve, waits up to 15 minutes for the load balancer health check, bakes templates if none are in the bucket, deploys the dashboard, and points api.<zone> at the LB IP through Cloudflare.
Subcommands
./deploy/deploy-gcp-multi.sh up # build + apply + bake + DNS
./deploy/deploy-gcp-multi.sh status # cert status, LB IP, MIG instances, agent heartbeats
./deploy/deploy-gcp-multi.sh smoke # /healthz + /version, plus sandbox lifecycle if SMOKE_API_TOKEN is set
./deploy/deploy-gcp-multi.sh bake-templates # (re)bake templates on an agent VM and push to GCS
./deploy/deploy-gcp-multi.sh deploy-frontend # build the dashboard and deploy it to Cloudflare Pages
./deploy/deploy-gcp-multi.sh cutover # upsert the api A record to the LB IP
./deploy/deploy-gcp-multi.sh scale 3 # resize the agent MIG
./deploy/deploy-gcp-multi.sh down # terraform destroyRunning the script with no subcommand prints this list.
bake-templates accepts template names (bake-templates code-interpreter browser) and honours FORCE=1 to rebuild artifacts that already exist. It runs the bake on an agent VM over IAP and polls for up to 90 minutes, because a full bake takes 10–60 minutes.
smoke targets api.pandastack.ai literally. Edit the script or run the checks by hand if you deployed to your own zone.
Topology
up builds, in one project:
- A private VPC with separate edge and agent subnets, Cloud NAT, and Secret Manager
- An edge MIG (
e2-smallby default) running the API behind a global HTTPS load balancer with a Google-managed certificate - A stateful agent MIG (
n2-standard-8by default,Intel Cascade Lakeminimum CPU platform, nested virtualization on, 400 GiB pd-ssd boot disk, no public IP). This pool carries the durable volumes disk, which is why GCP forbids an autoscaler on it — scale it by hand withscale N. - An ephemeral agent pool, created at size
0with its autoscaler modeOFF. It exists but is inert until you setagent_eph_countandagent_eph_autoscaler_modeinterraform.tfvars. - A Cloud SQL instance, a ClickHouse VM, and a DB proxy VM with a static IP for managed databases
The agent boot disk is 400 GiB because each agent keeps a baked snapshot and rootfs on local disk for every public template.
Overrides
write_tfvars reads these from .env.local:
GCP_PROJECT=pandastack-production
GCP_REGION=us-central1
GCP_ZONE=us-central1-a
USE_PREEMPTIBLE=false
AGENT_MACHINE_TYPE=n2-standard-8
AGENT_COUNT=1
AGENT_MAX_COUNT=8
EDGE_COUNT=2
CLOUDFLARE_DOMAIN=pandastack.ai
SSH_PUBKEY_PATH=~/.ssh/id_ed25519.pub
SSH_ALLOWED_CIDR=1.2.3.4/32up overwrites infra/terraform/envs/dev-gcp-multi/terraform.tfvars on every run. Edits made directly to that file are lost — change .env.local instead, or run terraform apply yourself.
Sizing the fleet
Capacity is bounded by memory, not by the NATID slot pool. Firecracker cannot resize vCPU or RAM at snapshot restore, so each template's guest RAM is fixed when the template is baked:
| Template | Baked guest RAM |
|---|---|
base | 4096 MiB |
browser | 4096 MiB |
code-interpreter | 2048 MiB |
agent | 2048 MiB |
postgres-16 | 1024 MiB |
Concurrency per agent VM is roughly that VM's usable RAM divided by the baked RAM of the template you run, minus host overhead. base at 4 GiB is the expensive one, and it is also what git-driven Apps run on. When the fleet runs out of memory, sandbox creates return 502 — add agents with scale N before that happens.
Point clients at your deployment
export PANDASTACK_API=https://api.pandastack.ai
export PANDASTACK_API_KEY=pds_abc123def456...Replace the API URL with your own origin.