Skip to main content

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. It takes about 5 minutes. It also costs nothing while the service is idle, because scale-to-zero means no compute charge until the first request arrives.

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, which is a packaged copy of your app and everything it needs to run. Serverless deploys images, not source code. To deploy from source instead, use Functions or Agents, which build images for you. Your image must listen for HTTP on a port; the default is 8080. Is your image private and stored in Crusoe Container Registry? Then map the project to Crusoe Cloud first — see where your image can come from.
  • For the API path: a credential. That is either an API key (see service accounts and API keys) or a 12-hour session token from platformctl login. You send it as a bearer token — the word Bearer, a space, then the credential, in the Authorization header. Details in API authentication.
Use a pullable image reference

A registry is a server that stores container images. Reference your image either from a public registry, or from a private Crusoe Container Registry repository — including the repository the platform built into when it built an image for you. See where your image can come from.

Anything else is accepted at deploy time and then fails minutes later with an image pull error. The machines that run your container cannot reach it.

Where your image can come from​

Serverless pulls from two kinds of registry: public ones, which need no setup, and your own private Crusoe Container Registry, which needs the project connected to Crusoe Cloud first. Anything the platform built for you is already in the second kind.

SourceAn image reference looks likeHow an image gets there
Images the platform built for youregistry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-checkout-api:c5c2f25dYou do not push these. When the platform builds from your source — see Functions, Agents and deploy from source — it writes the image into a repository in your own Crusoe Cloud Registry, named cai-<project-short>-<workload>. Reference it when you want to redeploy an image the platform already built. It is a CCR reference, so the row below applies to it
A public registrydocker.io/library/nginx:1.27You push it wherever you normally do. No setup here, but the image must be readable without a password
Crusoe Container Registry (CCR) — your private imagesregistry.us-east1-a.ccr.crusoecloudcompute.com/checkout-api.7dhg29ls:v1Map the project to Crusoe Cloud first, then push the image to your repository

Private images: map your project to Crusoe Cloud first​

A private registry asks for a password, and the platform has no way to guess yours. Mapping your project to a Crusoe Cloud account is how it gets one.

Do this once, in the console. Open your project, choose Project Settings, and enter a Crusoe Cloud access key ID and secret key. The same thing over the API is PUT /v1/projects/{projectID}/crusoe-cloud. That route lives on the platform API, so use $CAI_API, not $CAI_SERVERLESS_API. The Crusoe Cloud integration page walks through it step by step.

Mapping does two things:

  1. It stores your Crusoe Cloud credential for the project.
  2. It creates a hidden image-pull credential in your project, named ccr-pull. The platform builds this credential for you. The username is your Crusoe account email. The password is a short-lived registry token that can do nothing but pull images. Your access key is never used as a registry password, because CCR does not accept it as one.

From then on, every deploy and every edit runs the same check by itself. It asks two questions. Is this project mapped? And is the image's host one that the ccr-pull credential can sign in to?

If both answers are yes, the platform attaches the credential for you. It does that by pointing the service at a ccr-pull service account that carries it. If either answer is no, it attaches nothing and uses your image reference exactly as written. That is why a public image is never touched.

You never create, name, or reference that credential yourself. There is no field on this API for supplying your own registry password.

An unmapped project fails silently

If the project is not mapped, a CCR deploy is still accepted with 202. No credential is attached, and nothing in the response mentions the mapping. Minutes later the image pull fails, and the service never reaches ready. Map the project before you deploy a CCR image.

Already deployed, and mapped afterwards? PATCH the service so the check runs again and the new revision gets the credential. Re-sending the same image is enough.

Two more sharp edges before you rely on CCR:

  • The ccr-pull credential is built only during the mapping call. A repository you create later is not added to it.
  • The token inside the credential expires, which breaks pulls that had worked for weeks.

The fix for both is the same one line: map the project again. Known issues and the Crusoe Cloud integration page have the details.

One last rule. If you set service_account_name yourself, the platform leaves your choice alone and does not attach the CCR credential. Leave that field unset for CCR images. The serverless API reference covers both preconditions in full.

Deploy the service​

Only two things are ever required: a name and an image. A name is 1–52 characters of lowercase letters, digits, and dashes, starting with a letter and ending with a letter or digit, and it cannot be changed later. Everything else has a sensible default.

platformctl serverless create checkout-api \
--image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-checkout-api:c5c2f25d \
--env LOG_LEVEL=info
Never pass a credential with --env

An --env value lands in your shell history and shows up in the machine's list of running processes. Use --env-from-secret, which names the secret and nothing else.

Then watch it come up:

platformctl serverless list

You should see:

NAME STATE PUBLISHED URL
checkout-api ready no http://<private-hostname>

platformctl serverless get checkout-api -o json prints the full service object, exactly as the API returns it.

The CLI covers the whole lifecycle: create, update, delete, get, list, logs, metrics, revisions, set-traffic, spec, and triggers. It needs a project set, which you do with --project, the $CAI_PROJECT variable, or platformctl config set-project.

Leave a field out and the platform fills in a default, then echoes it back so you can see what you got:

FieldDefault
port8080
CPU / memory request250m / 512Mi
CPU / memory limit1 CPU / 512Mi
scaling.min_scale / max_scale / container_concurrency0 / 10 / 80
publish.enabledfalse
timeout_seconds300

One more thing happens on every deploy: the platform injects the environment variable CRUSOE_REQUEST_TIMEOUT_SECONDS into your container, matching timeout_seconds.

What your container receives from the platform​

Your image is your own — the platform does not add a runtime, a framework or an agent library to it. It does add four environment variables: your project's own credential, the two values needed to use it, and the request budget.

VariableWhat it is
CAI_API_URLHow your container reaches the platform API.
CAI_PROJECT_IDThe project your container is deployed in.
CAI_PROJECT_KEYYour project's own credential, formatted cai_wl_<keyid>_<secret>.
CRUSOE_REQUEST_TIMEOUT_SECONDSThe request budget, matching timeout_seconds.

There is nothing to configure: they are there on the first deploy, and a container built from source gets exactly the same four.

What the credential lets your container do is read its own project's secrets, and nothing else. CAI_PROJECT_KEY holds no project authority: every project route answers 404 not found for it, its own project included. Its one capability is minting a short-lived, read-only token for this project's secrets — which is what the example below uses. So a leaked container environment is not a foothold on your project.

If your container needs to do more than read secrets — publish to a topic, query a collection, manage other resources — give it a credential that can. Create a service account, mint a key for it, and store that key as a secret your container reads with the code below. See service accounts and API keys.

Read a secret from your own image​

Store the secret once, then read it at request time. No platform library is involved — two HTTPS calls with your language's standard library:

printf %s "sk-live-..." | platformctl secrets put openai-api-key
import json, os, urllib.parse, urllib.request

def read_secret(name):
api = os.environ["CAI_API_URL"].rstrip("/")
project = urllib.parse.quote(os.environ["CAI_PROJECT_ID"])
mint = urllib.request.Request(
f"{api}/v1/projects/{project}/secrets:issue-token",
data=json.dumps({"ttl_seconds": 300}).encode(),
headers={"Authorization": "Bearer " + os.environ["CAI_PROJECT_KEY"],
"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(mint, timeout=30) as resp:
token = json.load(resp)
addr = token["read"]["addr"].rstrip("/")
path = token["read"]["path"].strip("/")
url = f"{addr}/v1/{path}/{urllib.parse.quote(name)}"
read = urllib.request.Request(url, headers={"X-Vault-Token": token["token"]})
with urllib.request.urlopen(read, timeout=30) as resp:
return json.load(resp)["data"]["data"]["value"]

Call it when a request arrives, not at import: a failure while your container is still starting takes the whole revision down instead of failing one request. The full version, with version pinning and the error cases, is in use secrets in workloads.

These four names are reserved

The platform writes all four at deploy. Setting one through --env or the env field is refused with 400, naming the variable, rather than accepted and then replaced behind your back. Use a different name for your own variables.

CAI_PROJECT_KEY is never returned by the API and never appears in the service's stored environment: the platform holds it and binds it into your container at start. GET on a service returns only the variables you set, so the console's edit dialog shows you exactly the ones you can change.

If your project has never deployed anything, the credential may not exist yet on your very first deploy. In that case all three arrive absent rather than half-set, and a platform call fails with a clear error instead of silently using the wrong identity. Redeploying once the project has a credential picks it up.

Update a service​

Every edit creates a new revision.

platformctl serverless update checkout-api --image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-checkout-api:9f31ab02
--env on update replaces the whole environment

Unlike the individual fields, --env is not a merge on update — it replaces everything. Resupply every variable you want to keep.

scaling follows the same rule as every other field in a PATCH. Send a scaling block and any sub-field you leave out keeps the service's current value, not a platform default. So a PATCH carrying only {"min_scale": 2} leaves max_scale and container_concurrency exactly as they were.

Create behaves differently, because a brand-new service has no current scaling to keep. There, any sub-field you omit gets the platform default of 0 / 10 / 80.

Read whether it is serving​

Everything the platform observed about your service sits at the top level of the object, alongside the settings you sent. There is no status block to open — the readiness answer is spelled the same way here as on every other resource on the platform.

Four things tell you almost everything:

  • state — the one-word summary, always lowercase. pending means the platform is still working toward your spec. ready means it is serving as requested. not_ready means it is not serving. degraded means it is serving, but not with every setting you asked for. invalid means the spec was rejected. This is the word to show a person.
  • ready — a plain true/false, and the field to branch on in a script. It is true only for state: ready. When it is false, message carries one sentence saying why; when it is true, message is left out entirely.
  • conditions — the named pass/fail checks behind the state: Ready, ServiceReady, VisibilityEnforced, RuntimeClassApplied, TrafficAccepted, and Exposed. message is the Ready condition's own words; when you need more than one sentence, this is where the rest is.
  • generation vs. observed_generation — generation counts your edits; observed_generation counts the ones the platform has applied. If they differ, your latest change was accepted but has not taken effect yet. The console shows a notice for this state.

One more pair is worth knowing about, because the two look alike and mean different things. traffic is the split you asked for. active_traffic is the split actually being served right now. They match once a change has rolled out, and differ while one is on its way — providing real-time visibility into traffic routing status.

The console's service detail page shows all of this on its Overview tab. Three more tabs sit beside it: Revisions, which holds the traffic controls and one-click rollback; Logs; and YAML, which prints the service definition with secret values removed.

Work out what one service costs your quota​

A quota is a cap on how much of something your project may use at once. Every project starts with these: 50 running instances, 30 services, 10 CPU / 20 GiB reserved, and 20 CPU / 40 GiB maximum. The services cap is the one that surprises people, because of how revisions work:

  • Every revision of a serverless service permanently holds 2 services, which are internal routing objects. It holds them for as long as the revision exists.
  • Every deploy and every edit creates a new revision.

So a service you deployed and then updated once has 2 revisions, which hold about 4 of your 30 services. The count keeps growing with every edit until old revisions are cleaned up.

At 30 of 30, new revisions jam across the whole project with SKSReady=NotReady: No Private Service Name. 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. That route is on the platform API, so use $CAI_API, not $CAI_SERVERLESS_API; see API authentication. See all platform limits.

The CPU/memory trap

Set a CPU or memory request but no limit, and the platform stamps a default limit of 2 CPU / 4 GiB onto your container. It stamps the same limit onto the small helper container that handles your service's networking, which quietly drains your quota. You find out 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, and this is the only place this API uses 403. A delete removes the endpoint, releases its public hostname, and frees the quota held by all of its revisions.

platformctl serverless delete checkout-api

Next steps​