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.
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.