Deploy from CI with an API token
Every guide in this section does the same four things, in any CI system:
- build an image and push it to a registry, with one immutable tag per commit;
- tell Zaroz to run that image (
PUT /service-instances/{id}/source); - start a deployment and wait for it (
POST /service-instances/{id}/deployments); - fail the job if the rollout failed, and put the previous image back.
Steps 2 to 4 are one small script, zaroz-deploy.sh. The GitHub, GitLab and Forgejo guides only differ in how they build, where the secrets live and how they stop two deploys running at once.
| CI system | Guide |
|---|---|
| GitHub Actions | GitHub Actions |
| GitLab CI | GitLab CI |
| Forgejo Actions and Gitea Actions | Forgejo and Gitea Actions |
| anything else (Jenkins, Drone, a cron on your server) | Generic shell |
Create a token that can only deploy
CI should never hold a token that can read your invoices or delete your account. Give it a token limited to one service instance.
services.deploy is the deploy permission. It allows exactly two writes on a service: PUT /service-instances/{id}/source and POST /service-instances/{id}/deployments. It does not read anything, change environment variables, backups, files or the firewall, start, stop or restart the service (services.power), open a terminal, or touch billing or members. Add services.read so the job can read the source and wait for the deployment to finish. Which token to use depends on how narrow you need it:
- A personal token (
POST /me/api-tokens, below) can be limited to one service instance. It acts as the person who created it and needs a full signed-in session to mint: an API token cannot mint another. - An organization token (
POST /organizations/{id}/api-tokens) belongs to the organization and can outlive its creator ("lifetime": "organization_owned"), but every permission in it is scoped to the whole organization, never to one service.GET /organizations/{id}/api-tokens/grantable-capabilitieslists what you may put in it. Use it for a team pipeline that deploys several services.
curl
Code
TypeScript
Code
Python
Code
resource_idis the service instance id, the same UUID that is in the Workload's URL in the console.- Give the token an
expires_atand put a rotation reminder on it. A personal token is revoked withDELETE /me/api-tokens/{token_id}. - The secret (
zaroz_...) is shown once. Store it straight into your CI's secret store. Never put it in the repository, in.envfiles that are committed, or in the image. - Keep the secret out of command-line arguments, which any user on a shared runner can read with
ps. Pass it as an environment variable from the CI secret store, as the workflows do; the script sends it tocurlthrough standard input. - Mint one token per service and per environment (staging, production), so that revoking one does not stop the others.
A personal token acts as the person who created it: if that person leaves the organization or loses access to the service, the token stops working. For a team, prefer an organization token with lifetime: "organization_owned" (it survives whoever created it, but covers every service of the organization):
Code
Minting needs an Owner or Admin membership. Omit lifetime and the token is revoked when your membership ends.
What the token does at each step
| Step | Request | Permission |
|---|---|---|
| Read what runs now | GET /service-instances/{id}/source | services.read |
| Choose the image | PUT /service-instances/{id}/source | services.deploy |
| Deploy | POST /service-instances/{id}/deployments | services.deploy |
| Wait and check | GET /service-instances/{id}/deployments | services.read |
PUT /source does not deploy: it only saves what the next deployment will pull. POST /deployments takes no body and deploys what is saved, so there is no way to deploy something that was not saved first.
Rollout, success and rollback
A deployment has a status (how the operation ended) and an outcome (succeeded or failed, with a failure_reason such as cluster_rejected or subscription_not_active). succeeded means the rollout was applied; it does not say the new pods are healthy. So the script also reads workloads[].status from the same list and requires every workload to be running.
Rollback is a deploy of the previous image. The script reads running_image before changing anything and, if the new rollout fails, saves that image again and deploys it. Set ROLLBACK=0 to turn this off. A deploy is refused with 409 while a restore, a resize or another deployment holds the service, and the response says which operation and since when.
Zaroz does not watch a registry for a moved tag (watch_tag is always false), so a pipeline that pushes latest and deploys nothing will never update. Use a unique tag per commit, as the guides do.
Private images
If the image is private, add a pull-only registry token and pass it as REGISTRY_USERNAME and REGISTRY_PASSWORD. Zaroz stores it, never returns it, and uses it to pull from inside the cluster. It must not be the token your CI uses to push.
Example repositories
Each example has a ready deploy workflow for all three CI systems and runs end to end against a project of your own.
| Repository | What it shows |
|---|---|
example-scaled-api | an HTTP API on 1 to 8 autoscaled units behind one endpoint |
example-queue-worker | workers that scale with a queue |
example-game-network | hub, lobbies, duels and plots (Minecraft), phase 2 |
example-job-runner | one unit per job, started by key, result by event |
They live in the zaroz-oss organization at https://git.zaroz.cloud/zaroz-oss/.
