Documentation Index
Fetch the complete documentation index at: https://docs.phala.com/llms.txt
Use this file to discover all available pages before exploring further.
Automating deployments means every push to your main branch updates your CVM without manual intervention. This guide covers four approaches: the Phala CLI in GitHub Actions, the JS SDK in GitHub Actions, a GitLab CI pipeline, and Terraform in CI.
Prerequisites
- A Git repository with your application code and a
docker-compose.yml
- A Phala Cloud account with an API key
- A container registry account (Docker Hub, GHCR, or similar)
Secrets Management
Regardless of which CI platform you use, store your Phala Cloud API key as a secret. Never hardcode it in workflow files.
GitHub: Go to Settings > Secrets and variables > Actions and add:
| Secret | Description |
|---|
PHALA_CLOUD_API_KEY | Your Phala Cloud API key |
DOCKER_REGISTRY_USERNAME | Container registry username |
DOCKER_REGISTRY_PASSWORD | Registry password or access token |
GitLab: Go to Settings > CI/CD > Variables and add the same values as masked variables.
Rotate your API key immediately if it’s ever exposed in logs or committed to a repository. Generate a new one from Settings > API Keys in the Phala Cloud dashboard.
GitHub Actions with CLI
This is the simplest approach. The workflow builds your Docker image, pushes it to a registry, and deploys with phala deploy.
name: Deploy to Phala Cloud
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKER_REGISTRY_USERNAME }}/my-app:${{ github.sha }}
- name: Update compose with image tag
run: |
sed -i "s|\${DOCKER_IMAGE}|${{ secrets.DOCKER_REGISTRY_USERNAME }}/my-app:${{ github.sha }}|g" docker-compose.yml
- name: Install Phala CLI
run: npm install -g phala
- name: Deploy to Phala Cloud
env:
PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }}
run: phala deploy -c docker-compose.yml -n my-tee-app --wait
Your docker-compose.yml should reference the image variable:
services:
app:
image: ${DOCKER_IMAGE}
ports:
- "80:80"
The CLI detects existing CVMs by name. If my-tee-app already exists, it updates in place. Otherwise, it creates a new one.
GitHub Actions with JS SDK
If you need more control over the deployment (conditional logic, custom error handling, multi-step provisioning), use the SDK directly in a Node.js script.
Create a deploy script
Add scripts/deploy.mjs to your repository:import { createClient } from "@phala/cloud";
const client = createClient({
apiKey: process.env.PHALA_CLOUD_API_KEY,
});
const imageTag = process.env.IMAGE_TAG;
const appName = process.env.APP_NAME || "my-tee-app";
const compose = `
services:
app:
image: ${imageTag}
ports:
- "80:80"
`;
// Check if CVM already exists
const cvms = await client.getCvmList();
const existing = cvms.items.find((c) => c.name === appName);
if (existing) {
// Update existing CVM
await client.updateDockerCompose({
id: existing.id,
dockerCompose: compose,
});
await client.restartCvm({ id: existing.id });
console.log(`Updated CVM: ${existing.id}`);
} else {
// Provision new CVM
const provision = await client.provisionCvm({
name: appName,
composeFile: { dockerComposeFile: compose },
vcpu: 2,
memory: 4096,
diskSize: 20,
});
const cvm = await client.commitCvmProvision({
appId: provision.appId,
composeHash: provision.composeHash,
transactionHash: provision.transactionHash,
});
console.log(`Created CVM: ${cvm}`);
}
Create the workflow
name: Deploy with SDK
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Log in to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKER_REGISTRY_USERNAME }}/my-app:${{ github.sha }}
- name: Install dependencies
run: npm install @phala/cloud
- name: Deploy
env:
PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }}
IMAGE_TAG: ${{ secrets.DOCKER_REGISTRY_USERNAME }}/my-app:${{ github.sha }}
APP_NAME: my-tee-app
run: node scripts/deploy.mjs
GitLab CI
GitLab CI uses a similar pattern. Define a .gitlab-ci.yml in your repository root:
stages:
- build
- deploy
variables:
IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker build -t "$IMAGE_TAG" .
- docker push "$IMAGE_TAG"
deploy:
stage: deploy
image: node:20
script:
- npm install -g phala
- sed -i "s|\${DOCKER_IMAGE}|$IMAGE_TAG|g" docker-compose.yml
- phala deploy -c docker-compose.yml -n my-tee-app --wait
variables:
PHALA_CLOUD_API_KEY: "$PHALA_CLOUD_API_KEY"
only:
- main
GitLab’s built-in container registry works well here. The $CI_REGISTRY_* variables are available automatically in every pipeline.
For infrastructure-as-code workflows, run Terraform in your pipeline. This is especially useful when you manage replicas, instance types, or environment variables declaratively.
name: Deploy with Terraform
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.5"
- name: Terraform Init
run: terraform init
working-directory: ./infra
- name: Terraform Apply
env:
PHALA_CLOUD_API_KEY: ${{ secrets.PHALA_CLOUD_API_KEY }}
run: terraform apply -auto-approve -var="image_tag=${{ github.sha }}"
working-directory: ./infra
Your infra/main.tf would look something like this:
variable "image_tag" {
type = string
}
resource "phala_app" "web" {
name = "my-tee-app"
size = "tdx.medium"
region = "US-WEST-1"
image = "dstack-dev-0.5.7-9b6a5239"
disk_size = 40
replicas = 1
docker_compose = <<-YAML
services:
app:
image: myregistry/my-app:${var.image_tag}
ports:
- "80:80"
YAML
wait_for_ready = true
wait_timeout_seconds = 900
}
Store your Terraform state in a remote backend (S3, GCS, or Terraform Cloud) so that CI runs can access the same state file. Without remote state, each pipeline run would try to create a new CVM instead of updating the existing one.
Verify Deployment
After your pipeline completes, confirm the CVM is running:
# CLI
phala cvms get my-tee-app
# Or check the dashboard
# https://cloud.phala.com/dashboard
Troubleshooting
Authentication errors: Make sure PHALA_CLOUD_API_KEY is set correctly in your CI secrets. Test locally with phala status to verify the key works.
Build failures: Ensure your Dockerfile builds locally with docker build . before pushing to CI.
Deploy timeouts: If --wait times out, the CVM may still be starting. Check the dashboard or run phala cvms get to see the current status. Increase the timeout with --wait-timeout if your app takes longer to boot.
Image pull errors: Verify the image tag in your compose file matches what was pushed to the registry. For private registries, set DSTACK_DOCKER_USERNAME and DSTACK_DOCKER_PASSWORD as encrypted secrets on the CVM.