Deploy a service
In this guide you deploy a container image as a serverless service, watch it become ready, and learn exactly how much of your project's quota each deploy consumes. Takes about 5 minutes, and costs nothing while the service is idle — scale-to-zero means no compute charge until the first request.
Before you begin
- An account and a project. Ask your administrator for an account or an invitation link — there is no self-service sign-up. See projects and access.
- A container image. Serverless deploys images, not source code. (To deploy from source, use Functions or the Agent Engine, which build images for you.) Your image must listen for HTTP on a port — the default is 8080.
- For the API path: a credential. Either an API key (see service accounts and API keys) or a 12-hour session token from
platformctl login. Everything is sent asAuthorization: Bearer— details in API authentication.
Reference images by the pull prefix localhost:30500/<repo>:<tag>, or use a public registry. Do not use the in-cluster push address registry.cai-system.svc.cluster.local:5000 in a deploy — the service will be accepted, then fail minutes later with an image pull error, because the machines running your container cannot pull from that address.
Option A: deploy from the console
The web console is the main way to deploy today.
- Sign in and open your project, then choose Serverless in the navigation.
- Click Deploy service. The dialog asks for just three things up front:
- Name — 1–52 characters: lowercase letters, digits, and dashes; must start with a letter and end with a letter or digit. Names cannot be changed later.
- Container image — for example
localhost:30500/checkout-api:c5c2f25d. - Trigger type — leave as "HTTP only" unless you want the service fired on a schedule or by an event.
- Optionally expand Advanced options to set: container port (default 8080), protocol (HTTP/1.1, or
h2cfor gRPC), environment variables asKEY=valuelines, CPU and memory requests/limits, minimum instances (default 0), maximum instances (default 10), concurrent requests per instance (default 80), and a Publish to the internet checkbox (off by default). - Click through the Review & deploy confirmation. It lists every value, including the defaults it filled in for you, so there are no surprises. Confirm to deploy.
- Watch the status badge on the list page turn from Pending to Ready. The page polls automatically.
Option B: deploy with the API
There is no public API hostname yet in the alpha. Point $CAI_SERVERLESS_API at an endpoint your administrator provides, or port-forward the API yourself if you have cluster access.
Only two fields are required: name and image. Everything else has a sensible default.
export CAI_SERVERLESS_API=http://localhost:8080 # your admin-provided endpoint
export CAI_TOKEN=cai_... # your API key or session token
export PROJECT=<your-project-uuid> # from: platformctl projects list
curl -sS -X POST "$CAI_SERVERLESS_API/v1/projects/$PROJECT/services" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "checkout-api",
"image": "localhost:30500/checkout-api:c5c2f25d",
"env": {"LOG_LEVEL": "info"}
}'
You should see:
HTTP 202 Accepted
{
"name": "checkout-api",
"image": "localhost:30500/checkout-api:c5c2f25d",
"scaling": {"min_scale": 0, "max_scale": 10, "container_concurrency": 80},
"publish": {"enabled": false, "host": ""},
"timeout_seconds": 300,
"status": {"phase": "Pending", ...}
}
The response is 202 Accepted, not 201 Created — deliberate honesty: the platform accepted your request, but nothing is serving yet. Poll the service until status.phase is Ready:
curl -sS -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_SERVERLESS_API/v1/projects/$PROJECT/services/checkout-api" \
| jq '.status.phase, .status.internal_url'
You should see:
"Ready"
"http://checkout-api.<namespace>.svc.cluster.local"
Defaults the server applies when you omit them (and echoes back so you can see them): port 8080, CPU/memory requests 250m / 512Mi and limits 1 CPU / 512Mi, scaling 0 / 10 / 80, publish.enabled: false, and timeout_seconds: 300. The platform also always injects the environment variable CRUSOE_REQUEST_TIMEOUT_SECONDS into your container, mirroring timeout_seconds. Request bodies are capped at 256 KiB. The full field reference is in the serverless API reference.
Update a service
PATCH the same path with only the fields you want to change. Every edit creates a new revision:
curl -sS -X PATCH "$CAI_SERVERLESS_API/v1/projects/$PROJECT/services/checkout-api" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'Content-Type: application/json' \
-d '{"image": "localhost:30500/checkout-api:9f31ab02"}'
scaling follows the same sparse rule as everything else: if you send a scaling block, any sub-field you leave out keeps the service's current value, not a platform default — a PATCH that sends only {"min_scale": 2} leaves max_scale and container_concurrency exactly as they were. (Contrast with create: a brand-new service has no current scaling yet, so create fills any omitted sub-field with the platform default 0/10/80.)
Checking status: what the CLI can do
The platformctl CLI is read-only for serverless in the alpha — you can list and inspect, but deploying, editing, and deleting happen in the console or through the API.
platformctl serverless list
You should see:
NAME PHASE PUBLISHED URL
checkout-api Ready no http://checkout-api.<namespace>.svc.cluster.local
platformctl serverless get checkout-api -o json prints the full service object, exactly as the API returns it. The CLI needs a project set (--project, $CAI_PROJECT, or platformctl config set-project). Install it via install the CLI.
Reading the status block
Three things in status tell you almost everything:
phase— the one-word summary:Pending(still converging),Ready(serving as requested),NotReady(not serving),Degraded(serving, but not in the posture you asked for), orInvalid(the spec was rejected).conditions— the named pass/fail checks behind the phase:Ready,ServiceReady,VisibilityEnforced,RuntimeClassApplied,TrafficAccepted, andExposed. When something is wrong, the failing condition'smessagesays why in plain text.generationvs.observed_generation— if these differ, your latest change was accepted but has not been applied yet. The console shows a notice for this state.
The console's service detail page shows all of this on the Overview tab, plus Revisions (with traffic controls and one-click rollback), Logs, and a sanitized YAML tab.
Quota math: what one service costs your project
Every project has default caps: 50 running instances, 30 services, 10 CPU / 20 GiB reserved, and 20 CPU / 40 GiB maximum. The surprising one is the services cap, because of how revisions work:
- Every revision of a serverless service permanently holds 2 services (internal routing objects), for as long as the revision exists.
- Every deploy and every edit creates a new revision.
So a service you have deployed and then updated once holds 2 revisions ≈ 4 services of your 30 — and the count keeps growing with every edit until old revisions are cleaned up. At 30/30, new revisions across the whole project wedge with SKSReady=NotReady: No Private Service Name, and nothing becomes ready again until revisions are removed. See troubleshooting for the fix and quotas and audit for managing limits.
Your project's live number is always the authority — read it on the console's Quotas page, or from GET /v1/projects/{projectID}/quota on the platform API ($CAI_API, not $CAI_SERVERLESS_API — see API authentication). See all platform limits.
If you set a CPU or memory request but no limit, the project's LimitRange stamps a default limit of 2 CPU / 4 GiB onto your container — and onto its networking sidecar — silently draining your quota. It surfaces later as a FailedCreate error. Either set both request and limit, or set neither and take the platform defaults (250m/512Mi request, 1 CPU/512Mi limit).
Clean up
Deleting a service requires the project admin role (members get a 403 — the only place this API uses 403). Delete removes the endpoint, releases its public hostname, and frees all of its revisions' quota:
curl -sS -X DELETE "$CAI_SERVERLESS_API/v1/projects/$PROJECT/services/checkout-api" \
-H "Authorization: Bearer $CAI_TOKEN" -w '%{http_code}\n'
You should see:
204
In the console, the Delete button (admins only) confirms first and warns about any triggers that would be left pointing at nothing.
Next steps
- Autoscaling and scale to zero — tune instances, concurrency, and cold starts.
- Public endpoints and domains — publish your service to the internet.
- Serverless API reference — every endpoint, field, and error.