Compute Services

Deploy long-running services with public HTTPS hostnames using ccp compute.

Compute Services

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.

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 80

You'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.dev

Binary 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 musl-static (recommended for binary mode):
cargo build --release --target x86_64-unknown-linux-musl

# Deploy
ccp compute deploy --name hello --binary ./hello-server --port 8080

The CLI inspects the ELF before upload — non-ELF files and non-x86_64 binaries are rejected client-side. Glibc-linked binaries get a warning (they often run under alpine's libc6-compat shim, but musl-static is the recommended target).

 ✓ 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 -1

Inspect

ccp compute status
# Compute service hello
#
#   ID       3fd25c…
#   Status   running
#   Image    nginxdemos/hello       # or "Binary  binary:<short>"
#   Port     80
#   URL      https://hello.clusterbase.dev

Redeploy

Run compute deploy again — the recorded service_id in cluster.toml triggers an update instead of a fresh service. Mode is fixed at create time; 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-v2

The redeploy is a PATCH against the existing service; only the fields you pass are changed.

Tear down

ccp compute destroy

Deletes the service, its instance, the route, and the local cluster.toml so a future compute deploy in the same directory creates a fresh service. Accepts <NAME> or <UUID> as a positional argument when run from a different directory.

Project Shape: cluster.toml

The first ccp compute deploy writes a TOML manifest linking your local directory to the remote service. The format is hand-editable for everything except the [managed] block:

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)

[service]
internal_port = 8080
always_on = false

[env]
DATABASE_URL = "postgres://..."
LOG_LEVEL = "info"

[health]                              # optional; enables HTTP probes
path = "/healthz"                     # required when [health] is present

[managed]
service_id = "uuid"
organization_id = "uuid"
hostname = "hello.clusterbase.dev"

Subsequent ccp compute * commands read [managed].service_id and [managed].organization_id from this file when no flag is provided. Commit it — it's how other developers (and CI) find the same service.

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 [managed].

[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 retry

When set, deploy:

  1. 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.).
  2. 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.1 instead of 0.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.

cluster.toml is independent of .cluster/config.json (the serverless function link). A project may have one, the other, both, or neither. Projects upgraded from an older ccp will see their .cluster/compute.json auto-migrated to cluster.toml on the first command.

Deploy

ccp compute deploy [--name N] [--image I | --binary PATH] [--port P] \
                   [--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 cluster.toml and --service-id:

First deploy (create)

When there's no cluster.toml 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 cluster.toml exists (or --service-id is passed), ccp updates the existing service. Only the fields you pass change. Mode is immutable: redeploying an image-mode service with --binary (or vice versa) errors with a clear message — destroy and recreate to switch modes.

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

The --port cannot change after first deploy. To change ports, destroy and recreate.

If cluster.toml 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

FlagWhenNotes
--namefirst deploy1–64 chars, lowercase alphanumeric + dashes. Becomes the subdomain.
--imageimage modeContainer image reference (Docker Hub, GHCR, etc.). Mutually exclusive with --binary.
--binarybinary modeLocal path to a linux/amd64 ELF binary. Uploaded directly. Mutually exclusive with --image.
--portfirst deployPort your service listens on inside the VM.
--env K=VanyRepeatable. Sets/replaces a single environment variable per flag.
--always-onfirst deployDisable auto-pause when idle.
--service-idredeployUpdate a specific service by UUID, ignoring cluster.toml.
--org-idfirst deployRequired if you belong to multiple orgs and have no manifest.
-y / --yesanySkip interactive prompts. Combine with explicit flags for headless deploys.

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, and compute exec all 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.

Binary mode details

  • Bind to 0.0.0.0, not 127.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 returns 502 Bad Gateway. Bind to 0.0.0.0:PORT (or [::]:PORT for 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 warning. If your binary dynamically links against glibc, you'll see a warning. The platform's alpine VMs include libc6-compat (a glibc shim) which covers most Go binaries but can fall short on Rust binaries that pull in extended glibc symbols. Recommended: build with --target x86_64-unknown-linux-musl (Rust) or CGO_ENABLED=0 (Go) for a static binary that has no shim dependency.
  • Upload size limit: 50 MB. Larger binaries are tracked under #106 for chunked upload support.
  • Argv tail. Set [binary].args = [...] in cluster.toml to 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 from cluster.toml clears 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 ls

Prints one line per service: name, status, source (image ref or binary:<short>), hostname, last-update timestamp. Org resolves from --org-idcluster.toml → single-org auto-pick → interactive picker.

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) → cluster.toml → 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 ERROR

Live-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 -- env

Default 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 cluster.toml so a future compute deploy in the same directory creates a new service rather than failing on a gone reference.

Accepts either a UUID or a service name as the positional argument. If the local cluster.toml 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:

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 committed cluster.toml:
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 destroy

See 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

On this page