CI/CD and deployment automation

This guide covers the full pipeline from code push to running deployment: triggering builds automatically, deploying images to target instances, and choosing the right automation strategy for each environment.

Overview #

CI/CD for Heroic Cloud has two parts. Continuous Integration (CI) triggers Nakama builds automatically when code is pushed to your repository. Continuous Delivery (CD) deploys successful builds to target instances, either automatically or with manual approval.

CI/CD workflows apply to Nakama only. Satori doesn’t use the build pipeline. LiveOps changes go through the Satori console. See Satori deployments.

Prerequisites #

  • A Nakama Builder connected to your GitHub, GitLab, or Bitbucket repository. See Builders.
  • A service user with Trigger permission on the relevant builder.
  • Access to your repository’s CI/CD settings (for example, GitHub Actions secrets).

Part 1: Continuous integration #

The goal is to trigger a Heroic Cloud build automatically every time code is pushed to your repository.

Step 1: Create a service user #

  1. Create a service user in Heroic Cloud (for example, named “GitHub Actions CI”).
  2. Copy the service user’s secret token. Store it securely as it’s only shown once.
  3. Assign Trigger permission on the relevant builder, and add the service user to the relevant builder. This is the only permission the service user needs. Follow the principle of least privilege.

See Access control for details on service user permissions.

Step 2: Store the token in your CI system #

For GitHub Actions:

  1. In your GitHub repository, navigate to Settings > Secrets and variables > Actions.
  2. Add the service user token as a repository secret (for example, HEROIC_CLOUD_TOKEN).

For GitLab CI, add the token as a CI/CD variable in your project settings.

Step 3: Create the CI workflow #

Create a workflow file that calls the Heroic Cloud API on each push, authenticated with the service user token.

The flow:

  1. A developer pushes code to the repository.
  2. Your CI system triggers the workflow.
  3. The workflow calls the Heroic Cloud API to trigger a build.
  4. The builder compiles the code and produces a container image.

The following example triggers the Heroic Cloud builder and polls for the result. Add it to your repository at .github/workflows/heroic-cloud-build.yml, replacing the env values with your own.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
name: "Trigger Heroic Cloud Builder"
on:
    workflow_dispatch:
    push:
        branches:
        - master
        - main
        - develop

jobs:
    build-deploy-heroic-cloud:
        runs-on: ubuntu-latest

        env:
            organization_name: "your-organization"
            builder_name: "your-builder-name"
            nakama_image: "heroiclabs/nakama-enterprise:<X.Y.Z-rN>"
            service_user_email: ${{ secrets.HEROIC_CLOUD_SERVICE_EMAIL }}
            service_user_secret: ${{ secrets.HEROIC_CLOUD_SERVICE_SECRET }}
            retry_interval: 30
            timeout_interval: 600

        steps:
        -   name: "Checkout code"
            uses: actions/checkout@v3

        -   name: "Get commit hash"
            run: |
                echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_ENV
                echo "branch_name=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV                

        -   name: "Trigger builder"
            env:
                SERVICE_EMAIL: ${{ env.service_user_email }}
                SERVICE_SECRET: ${{ env.service_user_secret }}
                BUILDER_NAME: ${{ env.builder_name }}
                ORGANIZATION_NAME: ${{ env.organization_name }}
                COMMIT_HASH: ${{ env.commit_hash }}
                BRANCH_NAME: ${{ env.branch_name }}
                NAKAMA_IMAGE: ${{ env.nakama_image }}
                RETRY_INTERVAL: ${{ env.retry_interval }}
                TIMEOUT_DURATION: ${{ env.timeout_interval }}
            run: |
                START_TIME=$(date +%s)
                END_TIME=$((START_TIME + TIMEOUT_DURATION))
                while true; do
                    RESPONSE=$(mktemp)
                    HTTP_STATUS=$(curl -s -o "$RESPONSE" -w "%{http_code}" -X POST \
                        "https://cloud.heroiclabs.com/v3/organization/$ORGANIZATION_NAME/builder/nakama/$BUILDER_NAME/trigger" \
                        -u "$SERVICE_EMAIL:$SERVICE_SECRET" \
                        -H "Content-Type: application/json" \
                        -d "{\"commit\": \"$COMMIT_HASH\", \"branch\": \"$BRANCH_NAME\", \"nakama_image\": \"$NAKAMA_IMAGE\"}")
                    RESPONSE_BODY=$(cat "$RESPONSE")
                    if [[ "$HTTP_STATUS" -eq 200 ]]; then
                        echo "Build triggered for $COMMIT_HASH on $BUILDER_NAME"
                        exit 0
                    else
                        ERROR_CODE=$(echo "$RESPONSE_BODY" | jq -r '.code')
                        if [[ "$ERROR_CODE" -eq 3 ]]; then
                            exit 0
                        fi
                        echo "Trigger failed with status $HTTP_STATUS. Retrying..."
                    fi
                    CURRENT_TIME=$(date +%s)
                    if (( CURRENT_TIME >= END_TIME )); then
                        echo "Timeout exceeded. Exiting."
                        exit 1
                    fi
                    sleep $RETRY_INTERVAL
                done                

        -   name: "Wait for build to complete"
            env:
                SERVICE_EMAIL: ${{ env.service_user_email }}
                SERVICE_SECRET: ${{ env.service_user_secret }}
                BUILDER_NAME: ${{ env.builder_name }}
                ORGANIZATION_NAME: ${{ env.organization_name }}
                RETRY_INTERVAL: ${{ env.retry_interval }}
                TIMEOUT_DURATION: ${{ env.timeout_interval }}
            run: |
                START_TIME=$(date +%s)
                END_TIME=$((START_TIME + TIMEOUT_DURATION))
                while true; do
                    RESPONSE=$(curl -s -X GET \
                        "https://cloud.heroiclabs.com/v3/organization/$ORGANIZATION_NAME/builder/nakama/$BUILDER_NAME" \
                        -u "$SERVICE_EMAIL:$SERVICE_SECRET" \
                        -H "Content-Type: application/json")
                    STATUS=$(echo "$RESPONSE" | jq -r '.last_build.status')
                    IMAGE=$(echo "$RESPONSE" | jq -r '.last_build.image')
                    LOGS=$(echo "$RESPONSE" | jq -r '.last_build.logs.entries')
                    if [[ "$STATUS" -eq 2 ]]; then
                        echo "Build completed. Image: $IMAGE"
                        echo "image=$IMAGE" >> $GITHUB_ENV
                        exit 0
                    elif [[ "$STATUS" -eq 1 ]]; then
                        echo "Build in progress. Retrying..."
                    elif [[ "$STATUS" -eq 3 ]]; then
                        echo "Build failed."
                        echo "$LOGS" | jq -r '.[]'
                        exit 1
                    fi
                    CURRENT_TIME=$(date +%s)
                    if (( CURRENT_TIME >= END_TIME )); then
                        echo "Timeout exceeded. Exiting."
                        exit 1
                    fi
                    sleep $RETRY_INTERVAL
                done                

Key parameters:

ParameterDescriptionExample
organization_nameYour Heroic Cloud organization slug.my-studio
builder_nameThe name of the builder to trigger.my-game-builder
nakama_imageThe Nakama on Heroic Cloud image version to build against.heroiclabs/nakama-enterprise:3.24.0-r1-mc
service_user_emailThe service user email. Store as a GitHub secret.ci-bot@my-studio.com
service_user_secretThe service user secret token. Store as a GitHub secret.mK9pLx2vNqRt5wYz
retry_intervalSeconds to wait between polling attempts (default: 30).30
timeout_intervalMaximum seconds to wait before failing (default: 600).600

Store HEROIC_CLOUD_SERVICE_EMAIL and HEROIC_CLOUD_SERVICE_SECRET as encrypted secrets in your GitHub repository. Never commit credentials to your repository.

What happens if the build fails? #

A failed build doesn’t affect any running deployments. The failed image is never deployed anywhere. Your existing instances continue running the last successfully deployed image. Fix the issue in your code and push again.

Part 2: Continuous delivery #

Once builds trigger automatically, you need a strategy for getting those images onto your Nakama instances.

Auto-deploy #

Auto-deploy connects a builder to one or more Nakama instances. When a build completes successfully, the resulting image deploys to the specified targets automatically, with no manual intervention.

Configure auto-deploy from the builder settings menu to target one or more instances.

Auto-deploy limitations #

Auto-deploy is currently a beta/best-effort feature. The deployment may not proceed if the infrastructure isn’t functioning correctly. For this reason, use auto-deploy for development and QA environments only. Always require manual deployment to production.

Auto-deploy is available for Nakama only. Satori doesn’t have auto-deploy.

Manual deployment #

Deploy any completed build manually from the builder page or from the Nakama deployment itself. Both methods use a rolling update with no downtime.

EnvironmentDeploy methodWhy
DevelopmentAuto-deployFast iteration. Every successful build deploys immediately.
QAAuto-deployQA always has the latest build for functional testing.
ProductionManual onlyRequires explicit human approval after QA validation. This is a deliberate gate.

The key principle: automate everything up to production, then require a human decision for the final promotion. This gives you speed during development and safety for live traffic.

The complete flow #

With CI and CD fully configured:

  1. A developer pushes code to the repository.
  2. GitHub Actions (or GitLab CI) calls the Heroic Cloud API to trigger a build.
  3. The builder compiles the container image.
  4. Auto-deploy pushes the image to development and QA instances.
  5. The QA team validates the build on their instance.
  6. A release engineer manually deploys the validated image to production.

See also #