Zaroz
Operate services

Operate a running service

A service instance is what a subscription runs: a game server, a website, a database, a Workload. This guide is the daily API for any of them. What a particular service supports depends on its product: read its capabilities first (GET /service-instances/{id}/capabilities), and expect 404 for a feature the product does not offer. The samples use the helper from Using the Zaroz API.

Find your services

curl

TerminalCode
curl -sS "$ZAROZ_API/service-instances?project_id=$PROJECT&page_size=50" -H "$AUTH" curl -sS "$ZAROZ_API/service-instances/$SERVICE" -H "$AUTH" curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources" -H "$AUTH" curl -sS -X PATCH "$ZAROZ_API/service-instances/$SERVICE" -H "$AUTH" -H "Content-Type: application/json" -d '{"name": "Survival"}'

TypeScript

TypeScriptCode
import { call } from "./zaroz"; const list = await call("GET", `/service-instances?project_id=${PROJECT}`); for (const s of list.body.data) console.log(s.id, s.name, s.run_state, s.subscription_status); const res = await call("GET", `/service-instances/${SERVICE}/resources`); const main = res.body.data.find((r: any) => r.is_declared_primary); const RESOURCE = main.id; // the id the logs, metrics and files routes use

Python

Code
from zaroz import call _, listing = call("GET", f"/service-instances?project_id={PROJECT}") for s in listing["data"]: print(s["id"], s["name"], s["run_state"], s["subscription_status"]) _, res = call("GET", f"/service-instances/{SERVICE}/resources") RESOURCE = next(r["id"] for r in res["data"] if r["is_declared_primary"]) # used by logs, metrics and files

run_state is not_provisioned, provisioning, running, stopping, suspended, crash_looping, degraded or failed. It is read from the database, not from a live cluster call, so check availability: cluster_unavailable means it could not be refreshed right now. Renaming (PATCH) never changes the slug; a slug change is a separate, explicit field because it moves every URL pointing at the service.

Power

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/start" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/stop" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/restart" -H "$AUTH"

TypeScript

TypeScriptCode
const r = await call("POST", `/service-instances/${SERVICE}/restart`); if (r.status === 409) console.log("busy:", r.body.code); // operation-in-progress, restore-in-progress

Python

Code
status, body = call("POST", f"/service-instances/{SERVICE}/restart") if status == 409: print("busy:", body["code"]) # operation-in-progress, restore-in-progress

start and stop are explicit rather than a toggle, so two callers that both read "running" cannot cancel each other out. Stopping powers the service off and keeps its volume; it does not cancel or delete anything. A subscription that is suspended, cancelled, terminated or failed answers 404 here: pay what is owed first. While another operation holds the service you get 409 operation-in-progress naming which operation and since when; during a backup restore the code is restore-in-progress. The needed permission is services.power.

Logs

The API reads the retained window of one container's output, oldest first. An empty answer means "not in the retained window", never "it never happened"; the coverage field says in a sentence what was read, so show it beside the lines.

curl

TerminalCode
L=/service-instances/$SERVICE/resources/$RESOURCE/logs curl -sS "$ZAROZ_API$L?limit=200" -H "$AUTH" curl -sS -G "$ZAROZ_API$L" -H "$AUTH" --data-urlencode "search=error" --data-urlencode "cursor=$NEXT"

TypeScript

TypeScriptCode
let cursor: string | undefined; do { const q = new URLSearchParams({ limit: "500", search: "error", ...(cursor ? { cursor } : {}) }); const { body } = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/logs?${q}`); for (const line of body.lines) console.log(line.at, line.message); cursor = body.next_cursor ?? undefined; } while (cursor);

Python

Code
from urllib.parse import urlencode cursor = None while True: q = {"limit": 500, "search": "error", **({"cursor": cursor} if cursor else {})} _, body = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/logs?{urlencode(q)}") for line in body["lines"]: print(line["at"], line["message"]) cursor = body["next_cursor"] if not cursor: break

search is a case-insensitive substring, not a regular expression, and it runs on the server over the whole retained window. cursor is opaque and resumes exactly; never build one. container picks a container when the pod has several (containers lists them) and previous=true reads the previous run, handy after a crash. Lines carry no stream or level: nothing on the platform parses a severity.

To follow output live, mint a short-lived signed URL and open it: POST .../logs/stream-ticket returns { "url", "expires_at" }. The grant is recorded in the organization's activity log. (For Workloads, see the longer retention in Logs and metrics for units.)

Metrics

curl

TerminalCode
curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources/$RESOURCE/metrics?window_minutes=15" -H "$AUTH"

TypeScript

TypeScriptCode
const m = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/metrics?window_minutes=15`); for (const s of m.body.series) console.log(s.id, s.limit, s.points.length); // cpu, memory, net_in, net_out

Python

Code
_, m = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/metrics?window_minutes=15") for s in m["series"]: print(s["id"], s["limit"], len(s["points"])) # cpu, memory, net_in, net_out

window_minutes is 5, 15 or 30 (the default); anything else is refused because retention is thirty minutes. Every window returns 60 buckets, so a bucket is 5, 15 or 30 seconds wide. A bucket with no samples has a null value so a chart can break the line. Each point is a pair of a timestamp and a value; series are keyed cpu, memory, net_in, net_out, and limit is the ceiling it is measured against (or null when there is none). Convert units yourself.

Environment variables

curl

TerminalCode
E=/service-instances/$SERVICE/environment curl -sS "$ZAROZ_API$E" -H "$AUTH" curl -sS -X PUT "$ZAROZ_API$E/DATABASE_URL" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"value": "postgres://…", "secret": true}' curl -sS -X POST "$ZAROZ_API$E/DATABASE_URL/reveal" -H "$AUTH" # recorded in the activity log curl -sS -X DELETE "$ZAROZ_API$E/OLD_FLAG" -H "$AUTH"

TypeScript

TypeScriptCode
const E = `/service-instances/${SERVICE}/environment`; const set = await call("PUT", `${E}/LOG_LEVEL`, { value: "debug", secret: false }); console.log(set.body.applies, set.body.pending); // "restart" etc.

Python

Code
E = f"/service-instances/{SERVICE}/environment" _, set_ = call("PUT", f"{E}/LOG_LEVEL", {"value": "debug", "secret": False}) print(set_["applies"], set_["pending"]) # "restart" etc.

One variable per call. A secret's value is always null when you list: you can write or reveal it, never read it from the list. Sending a secret with no value keeps the stored one. applies is the platform's verdict on when the change reaches the running container: immediately, restart (a new pod is needed; today every change on a container app) or deploy. pending says whether the re-render really happened: if it did not (for instance the subscription is not active yet) the old value is still live, which matters when you rotate a leaked credential. A change takes a configuration lease (409 while another operation holds the service) and shows in the deployment history as a config_change. ZAROZ_AUTH_TOKEN is reserved.

Files

curl

TerminalCode
F=/service-instances/$SERVICE/resources/$RESOURCE/files curl -sS -G "$ZAROZ_API$F" -H "$AUTH" --data-urlencode "path=/data" curl -sS -G "$ZAROZ_API$F/content" -H "$AUTH" --data-urlencode "path=/data/server.properties" curl -sS -X PUT -G "$ZAROZ_API$F/content" -H "$AUTH" -H "Content-Type: application/json" \ --data-urlencode "path=/data/motd.txt" -d '{"content": "Welcome\n"}'

TypeScript

TypeScriptCode
const q = new URLSearchParams({ path: "/data/motd.txt" }); await call("PUT", `/service-instances/${SERVICE}/resources/${RESOURCE}/files/content?${q}`, { content: "Welcome\n" }); const back = await call("GET", `/service-instances/${SERVICE}/resources/${RESOURCE}/files/content?${q}`); console.log(back.body.content);

Python

Code
from urllib.parse import urlencode q = urlencode({"path": "/data/motd.txt"}) call("PUT", f"/service-instances/{SERVICE}/resources/{RESOURCE}/files/content?{q}", {"content": "Welcome\n"}) _, back = call("GET", f"/service-instances/{SERVICE}/resources/{RESOURCE}/files/content?{q}") print(back["content"])

This is for text files up to 1 MiB (a larger file is refused with a message naming the cap, never truncated, and a file that is not UTF-8 is refused for the same reason). The path is absolute within the volume mount and the server normalises it. For anything bigger, binary or many files at once, use SFTP (GET /service-instances/{id}/sftp, mount it as a drive) or POST .../files/download-url for a signed download link.

Scheduled tasks

A schedule runs a pipeline of tasks on a cron expression. Five fields (minute, hour, day of month, month, day of week), a time zone name and an overlap policy: skip (do not start while the last run is still going), queue, or concurrent. Tasks run in order and the first failure ends the run: console_command, run_executable, power_action, create_backup, wait, at most 25. This is the simplest way to get a nightly backup:

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/schedules" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "Nightly backup", "cron_expression": "0 4 * * *", "timezone": "Europe/Madrid", "overlap_policy": "skip", "timeout_seconds": 3600, "action": {"kind": "tasks", "tasks": [{"kind": "create_backup"}]} }' curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/schedules/$SCHEDULE/runs" -H "$AUTH" # run it now curl -sS "$ZAROZ_API/service-instances/$SERVICE/runs" -H "$AUTH"

TypeScript

TypeScriptCode
const s = await call("POST", `/service-instances/${SERVICE}/schedules`, { name: "Nightly backup", cron_expression: "0 4 * * *", timezone: "Europe/Madrid", overlap_policy: "skip", timeout_seconds: 3600, action: { kind: "tasks", tasks: [{ kind: "create_backup" }] }, }); console.log(s.body.next_fire_at, s.body.upcoming_fire_times);

Python

Code
_, s = call("POST", f"/service-instances/{SERVICE}/schedules", { "name": "Nightly backup", "cron_expression": "0 4 * * *", "timezone": "Europe/Madrid", "overlap_policy": "skip", "timeout_seconds": 3600, "action": {"kind": "tasks", "tasks": [{"kind": "create_backup"}]}, }) print(s["next_fire_at"], s["upcoming_fire_times"])

The scheduler reads the new row within about ten seconds; the answer already tells you next_fire_at and the next times, so you can check your expression. A cron-job service's schedules use {"kind": "run_job"} instead of tasks. Each run is recorded (also the ones that did not execute): outcome is succeeded, failed, interrupted, skipped_overlap, skipped_condition or missed. A schedule can also carry guard conditions (a time window, a calendar exclusion, the previous run, an HTTP check, another schedule's success) and be paused. See Backups for what a backup does.

Activity

TerminalCode
curl -sS "$ZAROZ_API/service-instances/$SERVICE/activity?page_size=25" -H "$AUTH"
TypeScriptCode
const a = await call("GET", `/service-instances/${SERVICE}/activity?page_size=25`);
Code
_, a = call("GET", f"/service-instances/{SERVICE}/activity?page_size=25")

Newest first: what the owner did to this service (and by which person or token). Platform-internal events are excluded.

Share a service with someone

Give a person access to one service without making them a member of the organization. They see only what you grant, cannot re-share, and never see other services or the organization's members. Permissions are a list of: console, files, power, settings, databases, database_browse, database_rows, database_schema, external_access, schedules, billing (seeing the service is implied by any share). billing reads invoices, resizes and withdraws a scheduled cancellation; it never cancels, renews or pays.

curl

TerminalCode
S=/service-instances/$SERVICE/shares curl -sS -X POST "$ZAROZ_API$S" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"email": "[email protected]", "permissions": ["console", "power"]}' curl -sS "$ZAROZ_API$S" -H "$AUTH" curl -sS -X PATCH "$ZAROZ_API$S/$SHARE" -H "$AUTH" -H "Content-Type: application/json" -d '{"permissions": ["console"]}' curl -sS -X DELETE "$ZAROZ_API$S/$SHARE" -H "$AUTH" curl -sS "$ZAROZ_API/me/shared-service-instances" -H "$AUTH" # what was shared with ME

TypeScript

TypeScriptCode
const share = await call("POST", `/service-instances/${SERVICE}/shares`, { email: "[email protected]", permissions: ["console", "power"], }); console.log(share.body.status); // "pending" until they open "Shared with me"

Python

Code
_, share = call("POST", f"/service-instances/{SERVICE}/shares", { "email": "[email protected]", "permissions": ["console", "power"], }) print(share["status"]) # "pending" until they open "Shared with me"

Sharing needs the Owner or Admin role, and a product that offers it (shareable in its capabilities, otherwise 404). The answer is the same pending share whether or not the address has an account, and accepted shows once they open it. Editing permissions takes effect on their next request. Refusals: 409 already_shared, 409 share_limit_reached, 422 cannot_share_with_self, and 429 past 20 new shares an hour per service.

Last modified on