# Set up CI/CD for Nakama

**URL:** https://heroiclabs.com/docs/heroic-cloud/titles/cicd/
**Summary:** Automate your full pipeline from code push to running deployment — triggering builds, deploying images to target instances, and choosing the right automation strategy.
**Keywords:** ci/cd and deployment automation, heroic cloud, cicd
**Categories:** heroic-cloud, cicd, titles

---


# Set up CI/CD for Nakama

Manually triggering builds and deployments works for early development, but it doesn't scale with a team. CI/CD automates the path from code push to running deployment. *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. LiveOps changes go through the Satori console. See [Satori deployments](../satori-deployments/).

## Prerequisites

* A Nakama Builder connected to your GitHub, GitLab, or Bitbucket repository. See [Builders](../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](../../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.

```yaml
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:

{{< table name="heroic-cloud.concepts.titles.cicd.key-parameters" >}}

Store `HEROIC_CLOUD_SERVICE_EMAIL` and `HEROIC_CLOUD_SERVICE_SECRET` as [encrypted secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) 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

{{< note "important" >}}
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.
{{< /note >}}

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.

### Recommended deployment strategy

{{< table name="heroic-cloud.concepts.titles.cicd.deployment-strategy" >}}

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

* [Builders and repository setup](../builders/) for creating and configuring builders.
* [Nakama deployments](../nakama-deployments/) for deployment configuration.
* [Access control](../../access-control/) for service user permissions.

<!-- TODO: Create a section in the service users docs covering all available APIs, with full documentation on request/response format and authentication, including cURL examples. -->
