Skip to main content

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:
SecretDescription
PHALA_CLOUD_API_KEYYour Phala Cloud API key
DOCKER_REGISTRY_USERNAMEContainer registry username
DOCKER_REGISTRY_PASSWORDRegistry 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.
1

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}`);
}
2

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.

Terraform in CI

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.