Zaroz
API Tokens

Deploy with GitLab CI and restart a service

This example builds a Node.js project, uploads the build over password-authenticated SFTP, and restarts one Zaroz Cloud service through the API. The SFTP password transfers files; a separate fine-grained API token controls only the restart.

The calls used are POST /service-instances/{id}/restart and GET /service-instances/{id}/resources from the API reference. To deploy a container image instead of uploading files, see Deploy from CI/CD.

What you need

From the service's page in the console, collect the SFTP host, port, username, password, and destination path.

Create a dedicated API token with these two grants, both scoped to the exact service being deployed (its service instance id, as in the console URL). See API tokens for how to scope a token:

  • services.read — lets the job wait for the server to come back.
  • services.power — lets the job restart the server.

Do not give a deployment token terminal, billing, database, or account-wide access.

Add protected CI/CD variables

In GitLab, open Settings → CI/CD → Variables and add:

VariableTypeRecommended flags
ZAROZ_API_TOKENVariableMasked, protected
ZAROZ_SERVICE_IDVariableProtected
ZAROZ_SFTP_HOSTVariableProtected
ZAROZ_SFTP_PORTVariableProtected
ZAROZ_SFTP_USERVariableProtected
ZAROZ_SFTP_PATHVariableProtected
ZAROZ_SFTP_PASSWORDVariableMasked, protected
ZAROZ_SFTP_KNOWN_HOSTSFileProtected

Generate ZAROZ_SFTP_KNOWN_HOSTS from a trusted computer and verify the displayed host fingerprint before storing it. Do not use StrictHostKeyChecking=no in a deployment pipeline.

Protect the deployment environment

Protected variables are only available to protected branches or tags. Configure GitLab's production environment protection so an untrusted merge request cannot exfiltrate either credential.

Add .gitlab-ci.yml

This example expects npm run build to produce a dist/ directory. Change the build command and local directory for your project.

YAMLCode
stages: - build - deploy build: stage: build image: node:22-alpine script: - npm ci - npm run build artifacts: paths: - dist/ expire_in: 1 hour deploy-production: stage: deploy image: alpine:3.22 needs: - job: build artifacts: true environment: name: production rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' before_script: - apk add --no-cache curl jq lftp openssh-client - install -d -m 700 ~/.ssh - cp "$ZAROZ_SFTP_KNOWN_HOSTS" ~/.ssh/known_hosts - chmod 600 ~/.ssh/known_hosts script: - | export LFTP_PASSWORD="$ZAROZ_SFTP_PASSWORD" lftp -e " set cmd:fail-exit true; set sftp:connect-program \"ssh -a -x -o StrictHostKeyChecking=yes\"; open --env-password --user \"$ZAROZ_SFTP_USER\" -p \"$ZAROZ_SFTP_PORT\" \"sftp://$ZAROZ_SFTP_HOST\"; mirror --reverse --verbose --no-perms dist/ \"$ZAROZ_SFTP_PATH\"; bye " unset LFTP_PASSWORD - | api() { curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $ZAROZ_API_TOKEN" \ -H 'Accept: application/json' \ "$@" } API="https://kernel.zaroz.cloud/api/v2/service-instances/$ZAROZ_SERVICE_ID" # A restart in place: no stopped state to race against. api -X POST "$API/restart" > /dev/null sleep 5 ready=false for attempt in $(seq 1 30); do state="$(api "$API/resources" | jq -r '.data[] | select(.is_declared_primary) | .status')" echo "Waiting for the server: $state" case "$state" in running) ready=true break ;; failed|crash_looping) echo "The service is $state after the restart" exit 1 ;; esac sleep 2 done if [ "$ready" != true ]; then echo "The service did not come back within about 65 seconds" exit 1 fi echo "Deployment uploaded and service restarted"

Why the job restarts in place and then waits

restart bounces the server without passing through a stopped state that your script would have to observe, so there is no stop-then-start race. The restart takes time to finish, so the job reads the primary resource's status until it is running, and gives GitLab a useful failure reason if it becomes failed or crash_looping instead.

For software that supports graceful reloads, prefer its own reload mechanism. A stop/start cycle interrupts active players or users.

Adapt the upload

The SFTP batch above overwrites matching files but does not delete remote files that disappeared from dist/. If your release must be an exact mirror, deploy into a versioned directory and atomically switch releases from your application's own startup script.

Never upload secrets from the repository. Keep runtime secrets in the Zaroz dashboard or protected CI/CD variables.

Troubleshooting

The API returns 403

Confirm both token grants are scoped to the same UUID in ZAROZ_SERVICE_ID. The owning user must also still be allowed to control that service.

SFTP reports a host-key error

Update ZAROZ_SFTP_KNOWN_HOSTS only after verifying the new fingerprint through a trusted channel. Do not disable verification.

The upload works but the restart does not

Check that services.power was selected, not only services.read, and that the token has not expired.

The server does not return to running within about a minute

Let the job fail rather than repeatedly restarting it. Inspect the service in the console (and its logs) and retry the deployment once the cause is fixed.

Last modified on