Skip to content

Deploy with GitLab CI and restart an order

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

You can inspect the Get Order Status and Toggle Order endpoints in the Kernel OpenAPI reference.

What you need

From the order's dashboard, collect the SFTP host, port, username, password, and destination path.

Create a dedicated API token with these two grants, both scoped to the exact order being deployed:

  • orders.read — lets the job wait for the server to stop.
  • orders.server.toggle — lets the job stop and start 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:

Variable Type Recommended flags
ZAROZ_API_TOKEN Variable Masked, protected
ZAROZ_ORDER_ID Variable Protected
ZAROZ_SFTP_HOST Variable Protected
ZAROZ_SFTP_PORT Variable Protected
ZAROZ_SFTP_USER Variable Protected
ZAROZ_SFTP_PATH Variable Protected
ZAROZ_SFTP_PASSWORD Variable Masked, protected
ZAROZ_SFTP_KNOWN_HOSTS File Protected

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.

.gitlab-ci.yml
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/v1/orders/$ZAROZ_ORDER_ID"

      api -X POST \
        -H 'Content-Type: application/json' \
        -d '{"active":false}' \
        "$API/toggle"

      stopped=false
      for attempt in $(seq 1 30); do
        state="$(api "$API/status" | jq -r '.state')"
        echo "Waiting for stop: $state"

        case "$state" in
          Suspended|NotProvisioned)
            stopped=true
            break
            ;;
          Failed)
            echo "The order entered Failed state; refusing to start it automatically"
            exit 1
            ;;
        esac

        sleep 2
      done

      if [ "$stopped" != true ]; then
        echo "The order did not stop within 60 seconds"
        exit 1
      fi

      api -X POST \
        -H 'Content-Type: application/json' \
        -d '{"active":true}' \
        "$API/toggle"

      echo "Deployment uploaded and start requested"

Why the job waits between stop and start

The toggle endpoint changes desired state; the workload still needs time to shut down. Starting immediately after the stop request can race with Kubernetes termination. Polling the status endpoint makes the deployment predictable and gives GitLab a useful failure reason.

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_ORDER_ID. The owning user must also still be allowed to control that order.

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 orders.server.toggle was selected, not only orders.read, and that the token has not expired.

The server stays in Stopping for more than 60 seconds

Let the job fail rather than repeatedly toggling it. Inspect the order in the dashboard and retry the deployment after the workload finishes stopping.