Deploy confidential apps and CVMs on Phala Cloud with Terraform.
Start with phala_app. It deploys one CVM under a single app identity with a Docker Compose, optional encrypted environment, OS image, and instance size. That single CVM is the bootstrap — every Phala app is born with one. For stateful replica sets (Consul, Patroni, etcd, Kafka, …), pair phala_app with phala_app_instance resources to declare additional named slots that share the same app_id.
This provider is intentionally close to the Terraform ergonomics people expect from providers like DigitalOcean: catalog data sources, declarative compute resources, explicit power control, SSH key resources, and straightforward outputs such as app_id, cvm_ids, and endpoint.
You need:
- Terraform installed.
- A Phala Cloud account.
- A Phala Cloud API key.
- An SSH public key only if you want SSH access inside a deployment.
Get an API key from the Phala Cloud dashboard:
- Sign in to
https://cloud.phala.com. - Open
Settings->API Keysor your profile page. - Create a key and export it:
export PHALA_CLOUD_API_KEY="phak_xxx"Provider environment variables:
PHALA_CLOUD_API_KEY
This is the default path. It deploys one app with one CVM and gives you an app_id and public endpoint.
The example below uses concrete defaults:
size = "tdx.medium"region = "US-WEST-1"disk_size = 40(GB)image = "dstack-dev-0.5.7-9b6a5239"
These values were tested against real Phala Cloud on March 8, 2026. If you need different sizes, regions, or images, use the discovery examples in Common Tasks after you complete this first deploy.
terraform {
required_providers {
phala = {
source = "phala-network/phala"
version = "0.2.0-beta.1" # or newer published version
}
}
}
provider "phala" {}
resource "phala_app" "hello" {
name = "hello-phala"
size = "tdx.medium"
region = "US-WEST-1"
image = "dstack-dev-0.5.7-9b6a5239" # use a full image slug from data.phala_images
disk_size = 40 # GB
docker_compose = <<-YAML
services:
web:
image: nginx:stable
ports:
- "80:80"
YAML
wait_for_ready = true
wait_timeout_seconds = 900
}
output "app_id" {
value = phala_app.hello.app_id
}
output "endpoint" {
value = phala_app.hello.endpoint
}Run:
terraform init
terraform apply -auto-approve
terraform output
terraform destroy -auto-approveWhat success looks like:
- Terraform prints an
app_id. - Terraform prints a public
endpoint. - The app appears in your Phala Cloud dashboard and reaches running state.
A complete copy of this example also lives in examples/app-quickstart.
If you want to test unreleased provider code from this repo, skip this path and use Developer Mode below.
This repository uses the Apache 2.0 license, matching the other open source Phala Cloud components. See LICENSE.
data "phala_sizes" "all" {}
data "phala_regions" "all" {}
data "phala_images" "all" {
# optional region filter
# region = "us-east"
}
data "phala_nodes" "west" {
region = "us-west"
}
data "phala_account" "current" {}
data "phala_workspace" "current" {}
data "phala_attestation" "web" {
cvm_id = "app_abc123"
}For image selection, use the exact slug from data.phala_images. Do not shorten it to dstack-dev or dstack-dev-0.5.7.
data "phala_images" "west" {
region = "us-west"
}
output "image_slugs" {
value = data.phala_images.west.images[*].slug
}resource "phala_ssh_key" "laptop" {
name = "laptop"
public_key = file("~/.ssh/id_ed25519.pub")
}
locals {
chosen_size = data.phala_sizes.all.sizes[0].slug
chosen_region = data.phala_regions.all.regions[0].slug
}
resource "phala_app" "web" {
name = "my-phala-web"
size = local.chosen_size
region = local.chosen_region
env = {
APP_SECRET = "replace-me"
}
docker_compose = <<-YAML
services:
web:
image: nginx:stable
ports:
- "80:80"
YAML
wait_for_ready = true
wait_timeout_seconds = 900
}
resource "phala_cvm_power" "web_power" {
cvm_id = phala_app.web.primary_cvm_id
state = "running" # or "stopped"
wait_for_state = true
wait_timeout_seconds = 900
}For Consul, Patroni, etcd, or any cluster that needs stable per-member identity, declare members on phala_app and pair it with phala_app_instance resources keyed by name. The bootstrap CVM (created by phala_app itself) gets the name in phala_app.name; the matching phala_app_instance adopts it. The other slots are created via POST /apps/{app_id}/instances.
locals {
consul_members = ["consul-0", "consul-1", "consul-2"]
}
resource "phala_app" "consul" {
name = local.consul_members[0]
members = local.consul_members
size = data.phala_sizes.all.sizes[0].slug
region = data.phala_regions.all.regions[0].slug
docker_compose = file("${path.module}/consul-compose.yaml")
}
resource "phala_app_instance" "consul" {
for_each = toset(phala_app.consul.members)
app_id = phala_app.consul.app_id
name = each.value
}
output "consul_member_uuids" {
value = { for k, v in phala_app_instance.consul : k => v.vm_uuid }
}The slot identity (name) is durable: if the cloud replaces the CVM occupying a slot, the next refresh rebinds vm_uuid while the Terraform resource key stays the same. App-level updates to docker_compose/env/image are blocked at plan time in members mode — destroy and recreate the app to roll new compose across slots.
resource "phala_app" "api" {
name = "api-app"
size = data.phala_sizes.all.sizes[0].slug
region = data.phala_regions.all.regions[0].slug
docker_compose = <<-YAML
services:
api:
image: nginx:stable
ports:
- "80:80"
YAML
}
resource "phala_app" "consumer" {
name = "consumer-app"
size = data.phala_sizes.all.sizes[0].slug
region = data.phala_regions.all.regions[0].slug
docker_compose = <<-YAML
services:
app:
image: nginx:stable
ports:
- "80:80"
YAML
env = {
UPSTREAM_APP_ID = phala_app.api.app_id
UPSTREAM_ENDPOINT = phala_app.api.endpoint
}
}data "phala_nodes" "west" {
region = "us-west"
}
resource "phala_app" "pinned" {
name = "pinned-app"
size = data.phala_sizes.all.sizes[0].slug
node_id = data.phala_nodes.west.nodes[0].node_id
docker_compose = <<-YAML
services:
web:
image: nginx:stable
ports:
- "80:80"
YAML
}Use this only when developing the provider from source (dev overrides + local binary).
Most users should not set api_prefix or PHALA_CLOUD_API_PREFIX.
The provider defaults to the public Phala Cloud API. The base URL override exists for staging environments, local mocks, or temporary infrastructure migrations:
export PHALA_CLOUD_API_PREFIX="https://cloud-api.phala.com/api/v1"The repo includes a smoke fixture and make targets under examples/smoke. This is for coverage and regression testing, not for first-time user onboarding.
Read-only smoke (catalog data sources only):
make smoke-plan PHALA_API_KEY="phak_xxx" CREATE_RESOURCES=falseCreate + destroy smoke:
make smoke-apply \
PHALA_API_KEY="phak_xxx" \
CREATE_RESOURCES=true \
APP_NAME="tf-smoke-app" \
APP_REPLICAS=2 \
CREATE_CONSUMER_APP=true \
CONSUMER_APP_NAME="tf-smoke-consumer" \
CONSUMER_APP_REPLICAS=1 \
APP_ENV='{"APP_SECRET":"replace-me"}' \
CVM_POWER_STATE="stopped" \
WAIT_FOR_READY=false \
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"
make smoke-destroy \
PHALA_API_KEY="phak_xxx" \
CREATE_RESOURCES=true \
APP_NAME="tf-smoke-app" \
APP_REPLICAS=2 \
CREATE_CONSUMER_APP=true \
CONSUMER_APP_NAME="tf-smoke-consumer" \
CONSUMER_APP_REPLICAS=1 \
APP_ENV='{"APP_SECRET":"replace-me"}' \
CVM_POWER_STATE="stopped" \
WAIT_FOR_READY=false \
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"Notes:
makewrites a local Terraform CLI config at/tmp/phala-tf-dev/terraformrcwithdev_overridesso your global~/.terraformrcis unchanged.- Smoke variables can be overridden with
SIZE,REGION, andIMAGE. - Set
CVM_POWER_STATE=running|stoppedto exercisephala_cvm_powerin smoke tests. - Set
CREATE_CONSUMER_APP=trueto exercise cross-app wiring (UPSTREAM_APP_ID,UPSTREAM_ENDPOINT). WAIT_FOR_READY=falsecan be useful for infrastructure lifecycle tests when runtime boot latency is variable.
phala_appis the app + bootstrap CVM. Each app is born with exactly one CVM via Phala's two-step API:POST /cvms/provisionthenPOST /cvms.- For stateful replica sets, declare
members = [<slot names>]onphala_appand pair withphala_app_instanceresources keyed by name. See Deploy a stateful replica set. - Key outputs:
app_id,primary_cvm_id(the bootstrap CVM),cvm_ids(every CVM under the app),endpoint. - Create-time identity/placement fields:
kms(currentlyphalaonly;ethereum/baseplanned)custom_app_id+nonce(deterministic identity flow for PHALA KMS)node_id(maps to provisionteepod_id; discover viadata.phala_nodes)
- In-place updates on the bootstrap CVM: size, disk, OS image, docker compose, pre-launch script, encrypted env. In members mode these app-level updates are blocked at plan time (they only mutate the bootstrap CVM, not the named slots) — destroy and recreate to roll new compose across the set.
- Compose-file runtime settings are exposed as first-class attributes:
public_logspublic_sysinfopublic_tcbinfogateway_enabledsecure_timestorage_fs
- Changing compose-file runtime settings triggers compose provision/apply flow and CVM restart.
- SSH access is managed at the account level with the
phala_ssh_keyresource. The keys registered on your account are injected into CVMs at launch; there is no per-app SSH key field. storage_fsis immutable after initial deployment (zfsorext4); changing it forces replacement.disk_sizecan only grow (shrink is rejected).- CPU/RAM changes are supported through
sizeupdates. - Encrypted secret modes:
env(recommended): provider auto-derivesenv_keysand encrypts values before API calls.encrypted_env+env_keys(manual): pass-through encrypted payload mode.
- State caveat:
envis sensitive but still stored in Terraform state; use manualencrypted_envmode if plaintext state storage is unacceptable.
- Optional phase-2 fields for on-chain KMS env updates:
env_compose_hashenv_transaction_hash
- Mode rule:
envcannot be combined withencrypted_env/env_keysin the same resource.
- Manual encrypted fields:
encrypted_env(sensitive, pass-through hex blob)env_keys(allowed env keys)
- Force-new fields:
name,region,listed,storage_fs.
- Read-only attestation fetch by
cvm_id. - Returns:
is_online,is_public,error,compose_filetcb_info_json,app_certificates_jsonraw_json(full response)
- Backed by:
POST /cvms/{id}/startPOST /cvms/{id}/stopGET /cvms/{id}(read/drift detection)
stateacceptsrunningorstopped.- Delete is no-op (removes Terraform state only; does not change CVM runtime).
- Backed by:
POST /user/ssh-keysGET /user/ssh-keysDELETE /user/ssh-keys/{id}
nameandpublic_keyare immutable (replace on change), similar to DO-style patterns.
- Current maturity:
beta. - Detailed matrix: FEATURE_MATURITY.md
- Release history: CHANGELOG.md
- On-chain KMS create/update flows (BASE/ETHEREUM).
- Add richer filtering for data sources (
images,sizes,regions).
Release process and gates: RELEASE.md
make ci
make package-release VERSION=0.2.0Then push a release tag such as v0.2.0 or v0.2.0-beta.1 to trigger the GitHub release workflow.
This provider includes an OpenAPI-generated Go client in internal/phalaapi, sourced from:
https://cloud-api.phala.network/openapi.json
Regenerate it with:
go generate ./internal/phalaapiNotes:
- The upstream OpenAPI is currently
3.1.0, and codegen compatibility is improved by a normalization step inopenapi/normalize-openapi.jq. - Some SDK endpoints used by this provider (
/instance-types,/user/ssh-keys) are not currently present in the public OpenAPI schema; those remain on fallback HTTP path handling for now.