Compute Services
Deploy long-running services with public HTTPS hostnames using ccp compute.
Compute services are long-running deployments reached at <name>.clusterbase.dev. Each service runs on its own managed instance with a stable hostname, environment variables, and a deployment history you can inspect and roll forward. A service can optionally be attached to a Project or left standalone.
Compute is a sibling product to serverless functions: functions run JS/TS in V8 isolates with sub-millisecond cold starts, while compute runs anything you can ship as a container image or pre-built native binary — Go, Rust, Python, Node, whatever — for workloads that need long-lived processes, custom system dependencies, or non-JS runtimes.
Two deploy modes:
--image— point at a container image reference (Docker Hub, GHCR, etc.). Best for anything that already has a Dockerfile or that needs a custom system environment.--binary— upload a pre-built linux/amd64 ELF binary directly. No Docker required. Best for Go and Rust services where the binary is the entire deliverable.
Source autodetect (Dockerfile / go.mod / Cargo.toml / package.json) is tracked under #96.
Quickstart
Get a public-facing service live in under a minute. Two flavors below — pick the one that matches your workload.
Image quickstart
ccp compute deploy --name hello --image nginxdemos/hello --port 80You'll be prompted for any missing values and for an organization if you have more than one. On success, ccp writes cluster.toml to the current directory and prints the public URL:
✓ Service deployed
name hello
image nginxdemos/hello
status running
URL https://hello.clusterbase.devBinary quickstart
Build a Linux x86_64 binary, then upload it directly — no Dockerfile, no registry:
# Build (Go example)
GOOS=linux GOARCH=amd64 go build -o ./hello-server .
# Or Rust — either linkage works; musl-static targets Alpine, glibc targets Debian:
cargo build --release --target x86_64-unknown-linux-musl
# Deploy
ccp compute deploy --name hello --binary ./hello-server --port 8080The CLI inspects the ELF before upload — non-ELF files and non-x86_64 binaries are rejected client-side. The inspection also picks the runtime, so build whichever way your toolchain prefers — see Binary runtimes below.
✓ Service deployed
name hello
status running
URL https://hello.clusterbase.dev(Binary-mode deploys deliberately don't print the upload URL — it's an internal storage location, not a stable identifier.)
Verify
curl https://hello.clusterbase.dev | head -1Inspect
ccp compute status
# Compute service hello
#
# ID 3fd25c…
# Status running
# Image nginxdemos/hello # or "Binary binary:<short>"
# Port 80
# URL https://hello.clusterbase.devRedeploy
Run compute deploy again — the service_id recorded in .ccp/compute-link.json triggers an update instead of a fresh service. Image-mode services redeploy with a new --image, binary-mode services redeploy with a new --binary (or omit the flag to re-upload the path recorded in cluster.toml):
# Image mode
ccp compute deploy --image nginxdemos/hello:plain-text
# Binary mode — re-uploads the path recorded in cluster.toml
ccp compute deploy
# Binary mode with a different file
ccp compute deploy --binary ./dist/server-v2The redeploy is a PATCH against the existing service; only the fields you pass are changed.
Editing an immutable field in cluster.toml (mode, runtime, resources, port) no longer requires
manual teardown: ordinary ccp compute deploy detects the change, shows a replacement plan, and
asks for confirmation before proceeding. See Replace immutable configuration.
Tear down
ccp compute destroyDeletes the service, its instance, the route, and this directory's local link (.ccp/compute-link.json). cluster.toml is preserved byte-for-byte — it describes what to run, not what is deployed — so a future compute deploy in the same directory recreates the service from the same committed description. Accepts <NAME> or <UUID> as a positional argument when run from a different directory.
Project Shape: cluster.toml and the local link
The first ccp compute deploy writes a TOML manifest describing the service — this part is committed, so every clone builds the same thing:
name = "hello"
mode = "binary" # or "image"
[image]
ref = "nginxdemos/hello" # only when mode = "image"
[binary]
path = "./hello-server" # only when mode = "binary"
args = ["--bind", "0.0.0.0:8080"] # optional argv tail (binary mode only)
runtime = "alpine" # optional: auto | alpine | debian (default auto)
[service]
internal_port = 8080
always_on = false
[resources] # optional; omit for the default shape
vcpu = 2
memory_mb = 1024
[env]
DATABASE_URL = "postgres://..."
LOG_LEVEL = "info"
[health] # optional; enables HTTP probes
path = "/healthz" # required when [health] is presentWhich service this machine talks to lives separately, in .ccp/compute-link.json (gitignored):
{
"service_id": "uuid",
"organization_id": "uuid",
"hostname": "hello.clusterbase.dev"
}cluster.toml describes what to run; .ccp/compute-link.json says which deployed service this machine/environment is talking to. That split means a project can have a staging and a production compute service — each machine just carries its own link — and a teammate's ccp compute status never resolves your service by accident.
Subsequent ccp compute * commands read service_id / organization_id from .ccp/compute-link.json when no flag is provided. Commit cluster.toml; never commit the link — ccp init wires .ccp/ into .gitignore for you.
You can hand-edit [image].ref, [binary].path, [binary].args, [service].*, [env].*, and [health].* between deploys. Run ccp compute deploy to apply the changes. Don't hand-edit .ccp/compute-link.json.
A checkout with no link (fresh clone or CI)
A fresh clone has cluster.toml but no .ccp/compute-link.json. ccp compute deploy looks up the committed name in the org and links to that existing service instead of creating a duplicate — it only creates a new one when the name is free (confirmed interactively, or automatically with -y). --name overrides the lookup/create target for that deploy but is not written back to cluster.toml, so it stays per-environment. On a multi-org account in headless mode this lookup needs --org-id or CCP_ORG_ID, since the org is no longer implied by a committed [managed] block.
Migrating from an older ccp
Projects created with an older ccp have their link committed inside a [managed] table in cluster.toml. The next ccp compute deploy or ccp compute destroy moves service_id / organization_id / hostname into .ccp/compute-link.json and strips [managed] from cluster.toml, printing a notice to commit the change.
This is a one-way door that travels through git. ccp older than 0.1.68 requires [managed] and fails every compute command against a migrated cluster.toml with a missing field managed error — including on machines that never ran the new ccp. Upgrade every teammate, CI runner, and VM template to ccp 0.1.68+ before committing the stripped cluster.toml.
[health] — readiness probes (optional)
Without [health], ccp compute deploy checks only that something is listening on the configured port (TCP-level nc -z). With it, the deploy also runs an HTTP-level probe and detects a common deploy footgun where the service is bound to loopback only.
[health]
path = "/healthz" # required: HTTP path on the service
# port = 9090 # optional: defaults to [service].internal_port
# initial_delay_ms = 1000 # wait before the first probe attempt
# timeout_ms = 2000 # per-probe timeout
# period_ms = 500 # interval between attempts
# success_threshold = 1 # consecutive 2xx required
# startup_budget_ms = 30000 # total time the probe will retryWhen set, deploy:
- Polls
wget --spider http://127.0.0.1:<port><path>inside the VM until your service returns a 2xx (catches "port open but app not ready yet" — DB pools warming, JIT, migrations, etc.). - Sends the same request through the proxy from outside the VM. If this connect-refuses or times out while step 1 succeeded, your service is almost certainly bound to
127.0.0.1instead of0.0.0.0. The deploy succeeds but prints a yellow warning with the bind-fix hint.
This mirrors Kubernetes httpGet probes — opt-in, user-defined path, no platform default. Add it to existing services by editing cluster.toml and redeploying; the change persists on the API on next deploy.
[network] — private overlay networking (optional)
An always-on service can join an organization-scoped private network by adding a [network] table to cluster.toml:
[network]
name = "backend"The network is created automatically on first deploy. Services on the same network reach one another at <service>.<network>.internal regardless of which node they're placed on; services outside the network cannot resolve or connect to that private address. Both [service] name and [network].name must be lowercase DNS labels.
Private networking currently requires [service].always_on = true. Joining, changing, or removing the network on an existing service is an immutable-field change: ordinary ccp compute deploy detects it and offers a replacement (see Replace immutable configuration) rather than requiring manual destroy/recreate. It does not change the service's public <name>.clusterbase.dev route.
cluster.toml is independent of .ccp/config.json (the serverless function link). A project may have one, the other, both, or neither. Projects upgraded from a pre-#458 ccp will see their legacy .cluster/compute.json auto-migrated to cluster.toml on the first command; projects upgraded from a pre-#610 ccp will see the committed [managed] block moved into .ccp/compute-link.json — see Migrating from an older ccp above.
Deploy
ccp compute deploy [--name N] [--image I | --binary PATH] [--port P] \
[--runtime auto|alpine|debian] [--vcpu N --memory-mb M] \
[--env K=V]... [--always-on] \
[--service-id S] [--org-id O] [-y]--image and --binary are mutually exclusive. compute deploy decides between create and update by inspecting .ccp/compute-link.json and --service-id:
First deploy (create)
When there's no cluster.toml, no .ccp/compute-link.json, and no --service-id, ccp creates a new service. Required: --name, --port, and exactly one of --image / --binary. In an interactive terminal, missing values are prompted; in headless mode, missing flags error.
# Image mode
ccp compute deploy --name api --image ghcr.io/me/api:v3 --port 8080 \
--env DATABASE_URL=postgres://... --env LOG_LEVEL=debug
# Binary mode
ccp compute deploy --name api --binary ./target/release/server --port 8080 \
--env DATABASE_URL=postgres://...Redeploy (update)
When .ccp/compute-link.json exists (or --service-id is passed), ccp updates the existing service. Only the fields you pass change. Passing --image to a binary-mode service (or --binary to an image-mode service) is an immutable-field change: ccp detects it and errors, asking you to move the new mode/image/binary into cluster.toml and rerun ccp compute deploy — see Replace immutable configuration.
# Image mode redeploy
ccp compute deploy --image ghcr.io/me/api:v4 # roll a new image
ccp compute deploy --env LOG_LEVEL=info # update env
# Binary mode redeploy
ccp compute deploy --binary ./target/release/server-v2 # upload a new build
ccp compute deploy # re-upload the path in cluster.tomlThe --port cannot change after first deploy via redeploy flags. To change the port, edit [service].internal_port in cluster.toml and run ccp compute deploy; it detects the change and offers a replacement.
If .ccp/compute-link.json points at a service that's been deleted (e.g. destroyed from another machine), the next compute deploy detects the orphan and offers to recreate the service with the same name and configuration.
Flags
| Flag | When | Notes |
|---|---|---|
--name | first deploy | 1–64 chars, lowercase alphanumeric + dashes. Becomes the subdomain. |
--image | image mode | Container image reference (Docker Hub, GHCR, etc.). Mutually exclusive with --binary. |
--binary | binary mode | Local path to a linux/amd64 ELF binary. Uploaded directly. Mutually exclusive with --image. |
--runtime | binary mode | auto (default), alpine, or debian. See Binary runtimes. |
--vcpu + --memory-mb | first deploy | Optional VM shape; the flags must be paired. See VM resources. |
--port | first deploy | Port your service listens on inside the VM. |
--env K=V | any | Repeatable. Sets/replaces a single environment variable per flag. |
--always-on | first deploy | Disable auto-pause when idle. |
--service-id | redeploy | Update a specific service by UUID, ignoring the local link. |
--org-id | first deploy | Required if you belong to multiple orgs and have no manifest or CCP_ORG_ID. |
-y / --yes | any | Skip interactive prompts. Combine with explicit flags for headless deploys. |
Binary runtimes
Binary-mode services run on one of two runtimes: Alpine (musl) or Debian (glibc). Resolution precedence is --runtime → [binary].runtime in cluster.toml → auto. auto inspects the ELF and routes static and musl-linked binaries to Alpine and glibc-linked binaries to Debian, so most deploys never need the flag. An explicit runtime that cannot run the binary — --runtime alpine with a glibc ELF, or --runtime debian with a musl ELF — is rejected before upload.
After a successful deploy, ccp writes the concrete runtime back to [binary].runtime in cluster.toml. The runtime is immutable for an existing service: change [binary].runtime and run ccp compute deploy to move between Alpine and Debian — ccp detects the change and offers a replacement.
VM resources
By default a service gets its runtime's shape — 1 vCPU / 512 MiB for binary services. To pick a different one, pass --vcpu and --memory-mb together on first deploy, or set the [resources] table in cluster.toml. The published shapes are 1/256, 1/512, 2/1024, 4/2048, and 4/4096 (vCPU/MiB); anything else is rejected. The shape is immutable after create — edit [resources] in cluster.toml and run ccp compute deploy to change it, which detects the change and offers a replacement. ccp compute list and ccp compute status show the persisted shape.
Replace immutable configuration
Editing an immutable field in cluster.toml — mode, [binary].runtime, [service].internal_port,
[resources], [service].always_on, or [network] — no longer requires manually destroying and
recreating the service. Ordinary ccp compute deploy compares the manifest against the live service,
and if it finds an immutable change, prints a plan of the changed fields and asks for confirmation
before proceeding:
# After editing cluster.toml, review the plan and confirm interactively
ccp compute deploy
# Explicit consent for automation; non-interactive replacement requires -y or --yes
ccp compute deploy -y # --yes is equivalentReplacement reads the desired configuration only from cluster.toml and .env — pass --name,
--image, --binary, --runtime, --port, --vcpu/--memory-mb, --env, or --always-on
alongside an immutable change and ccp refuses, asking you to put the override in those files first. An explicit
--service-id must match this project's local link, and an explicit --org-id must match the
linked service's organization.
Under the hood, ccp prepares and uploads any binary before deleting the old service, then deletes
the old service and creates a new one. This is a real interruption: the old guest's filesystem and
deployment history are lost, and the service and VM get new IDs. The existing service name and
built-in <name>.clusterbase.dev hostname (including a per-environment name previously set with
--name) are preserved, and the committed cluster.toml is left byte-for-byte unchanged. Custom
domains that target the old VM must be rebound afterward. Omitting [resources] retains the
existing resource shape rather than resetting it.
The local link isn't updated until the API accepts the new service, and a local, context-scoped
recovery record (outside cluster.toml) tracks the original identity so a retry can resume safely.
If deletion or creation fails partway, fix the reported cause and rerun ccp compute deploy --yes
in the same project/context — keep cluster.toml, .env, and the local link untouched while
recovering. If the original service still exists, the retry shows the plan again, asks for
confirmation, and deletes it before creating the replacement. If the original is already gone, the
retry instead looks for an already-accepted service under the original name and, if it finds one,
recovers the link to it without deleting it or creating a duplicate. Use ccp compute status to
check a recovered service's readiness.
Auto-pause and wake
Compute services auto-pause after 10 minutes idle to free the underlying VM while you're not using it. The pause is transparent to most usage:
- A new HTTPS request to the service hostname wakes the VM automatically. The first request after a pause sees a brief wake latency (typically a few seconds); subsequent requests are normal.
ccp compute deploy(redeploy),compute restart,compute logs, andcompute execall transparently wake the service before sending their RPC — no manual unpause needed.
To opt out for services that need consistent latency on every cold request (e.g. a public-facing API that can't absorb the wake delay), set --always-on on first deploy or always_on = true in cluster.toml. Always-on services keep their VM running indefinitely.
A VM that Billing previously paused for non-payment performs a fresh admission check on the next wake attempt. If the account is now eligible, the wake resumes normally; otherwise the VM stays paused and the operation returns payment_required (HTTP 402).
Workload operations (compute deploy redeploy, restart, logs, exec) return service_not_running while initial deployment is still pending; wait for deployment to finish before retrying.
Binary mode details
- Bind to
0.0.0.0, not127.0.0.1. The platform's HTTP proxy runs on the host, in a different network namespace from your VM. A service bound to loopback (127.0.0.1:PORT) is reachable only from inside the VM — every public request returns502 Bad Gateway. Bind to0.0.0.0:PORT(or[::]:PORTfor IPv6) so the listener is on the VM's network interface. The most common cause of "deploy succeeded but my service won't load" is this one line. Image-mode services hit the same issue if the container's CMD binds to loopback. - ELF inspection runs client-side. Non-ELF and non-x86_64 binaries are rejected before upload. Architecture is detected from the ELF header.
- Glibc binaries run natively. No compat shim, no warning —
--runtime autoroutes glibc ELFs to Debian and musl-static (--target x86_64-unknown-linux-muslfor Rust,CGO_ENABLED=0for Go) to Alpine; see Binary runtimes. - Upload size limit: 256 MiB. ccp uploads through a signed URL and the VM downloads the binary directly from object storage, verifying its size and SHA-256 before swapping it in.
- Argv tail. Set
[binary].args = [...]incluster.tomlto pass argv to your binary on every deploy. Each list element becomes one argv entry — no shell parsing — so values can contain spaces, quotes,$, etc. without escaping. Removing the key fromcluster.tomlclears the args server-side on next redeploy. - Crash recovery. The binary runs under OpenRC's supervise-daemon, with crash-restart enabled (10 restarts in 5 minutes before the service is marked crashed). Logs are tailed via
ccp compute logs.
List Services
ccp compute list [--org-id O]
# alias: ccp compute lsPrints one line per service: name, status, source (image ref or binary:<short>), hostname, last-update timestamp. Org resolves from --org-id → .ccp/compute-link.json → CCP_ORG_ID → single-org auto-pick → interactive picker.
A malformed .ccp/compute-link.json fails loudly during this org lookup — including for ccp compute ls — rather than silently falling back to a different organization. Fix the link file or pass --org-id explicitly.
A service's hostname is carried as a clickable OSC 8 hyperlink rather than printed as a bare URL, unless output is piped or redirected — then it drops the link in favor of a plain trailing URL column. Pass --json for full ids, exact timestamps, and complete URLs with no styling, suitable for scripts.
Show Status
ccp compute status [SERVICE_ID|NAME]Prints metadata plus the last 10 deployments — each deployment's id-prefix, status (live / deploying / superseded / failed), source (image ref or binary:<short>), deploy time, and any error message. Useful for debugging a redeploy that didn't reach running.
Service identifier resolves from positional arg (UUID or service name) → .ccp/compute-link.json → interactive picker.
Tail Logs
ccp compute logs [SERVICE_ID] [-n TAIL]Prints the last N lines of stdout/stderr (default 100, max 1000). Pipe-friendly: each log line is a single line on stdout.
ccp compute logs -n 500 | grep ERRORLive-follow (-f) is not yet supported; for now, re-run periodically.
Run a One-Shot Command
ccp compute exec [SERVICE_ID] [--timeout-ms MS] -- <CMD> [ARGS...]Runs a single command inside the running container, forwards its stdout/stderr to your terminal, and exits with the container's exit code. The -- is mandatory — it separates ccp's flags from the command you're running.
ccp compute exec -- ls -la /app
ccp compute exec -- sh -c 'echo $DATABASE_URL | head -c 20'
ccp compute exec --timeout-ms 5000 -- envDefault timeout is 30 seconds. Long-running commands appear to hang until they exit.
Restart
ccp compute restart [SERVICE_ID]Re-runs the same image. Use after env changes that aren't picked up by hot reload, or to clear in-process state.
Destroy
ccp compute destroy [SERVICE_ID|NAME] [-y]Tears down the service, its instance, and the route. Asks for confirmation unless -y is passed or you're in headless mode (which auto-confirms). On success, deletes this directory's .ccp/compute-link.json (and a legacy committed [managed] block, if one is still present) — but only when the destroyed service is the one this directory is linked to. cluster.toml is preserved byte-for-byte, so ccp compute deploy can recreate the service from the same committed description.
Accepts either a UUID or a service name as the positional argument. If the local link already points at a deleted service, destroy succeeds with Compute service was already gone — cleaned up local manifest.
Headless Use
All ccp compute commands are headless-safe with CCP_HEADLESS=1 and a session token in CCP_SESSION_TOKEN. Compute also accepts an organization API key via CCP_API_KEY for organization-level automation — see Authentication:
export CCP_HEADLESS=1
export CCP_SESSION_TOKEN=$(... fetched from operator's machine ...)
# First deploy (image mode) needs explicit flags — no prompts in headless mode:
ccp compute deploy --name my-api --image ghcr.io/me/api:v3 --port 8080 --org-id "$ORG"
# Or binary mode — binary is uploaded directly:
ccp compute deploy --name my-api --binary ./target/release/server --port 8080 --org-id "$ORG"
# Redeploy reads service_id from the local .ccp/compute-link.json:
ccp compute deploy --image ghcr.io/me/api:v4
ccp compute deploy --binary ./target/release/server-v2
# Destructive commands auto-confirm — verify the target before invoking:
ccp compute destroySee Headless mode for the full pattern.
Custom Domains
A registered domain can be pointed at a compute service via its underlying instance id. Get the id from ccp compute status, then:
ccp domain link example.com --vm "<instance_id>:<port>"The <port> is the same one passed to ccp compute deploy --port. See Domains for the full flow.
Next Steps
- Headless deploys — wire compute into CI / agent workflows
- Custom domains — serve compute services from your own domain
- Database — pair a managed Postgres with your compute service