Mental model
vs. Docker Compose
A Project maps directly to a
docker-compose.yml file. Each ServiceDef
is a Compose service; each VolumeDef is a named volume. The key
difference is that Caravanserai schedules the entire project onto a cluster
node rather than running it locally.vs. Kubernetes
A Project is closest to a Kubernetes Pod (co-located containers, shared
network) combined with a Deployment (desired-state reconciliation).
Caravanserai does not scatter services across nodes — all services in a
project always land on the same node.
ProjectSpec fields
spec is the desired state you declare. The control plane and agent reconcile
the cluster toward it continuously.
services
A list ofServiceDef objects — the containers that make up the project.
Every project must have at least one service.
| Field | Type | Description |
|---|---|---|
name | string | Unique name within the project. Used as the DNS hostname on the shared bridge network. |
image | string | Docker image reference, e.g. postgres:15. |
env | EnvVar[] | Environment variables to inject at container start. |
volumeMounts | VolumeMount[] | Volumes to attach. Each entry specifies a name (matching a VolumeDef) and a mountPath inside the container. |
volumes
A list ofVolumeDef objects — named storage units that one or more services
can mount.
| Field | Type | Description |
|---|---|---|
name | string | Unique name within the project. Referenced by volumeMounts[*].name. |
type | string | Volume lifecycle. Currently only Ephemeral is supported. |
An
Ephemeral volume is created when the project starts and deleted when
the project is stopped or moved. It does not survive rescheduling. If you
need durable storage, manage the volume on the node directly and use a
host path (not yet supported in the API; track the roadmap for HostPath
support).ingress
A list ofIngressDef objects — HTTP routing rules that expose a service
through the cluster’s ingress layer.
| Field | Type | Description |
|---|---|---|
name | string | Unique name for this ingress rule. |
host | string | Hostname for the rule. If it contains a dot it is used verbatim; otherwise the final hostname is assembled as {host}.{environment}.{baseDomain}. |
target.service | string | Name of the service to route traffic to. |
target.port | integer | Port on the target service. |
access.scope | string | Visibility scope. Currently only Internal is supported, routing traffic over the Headscale overlay network only. |
expireAt
An optional RFC 3339 timestamp. When set, the garbage-collection controller deletes the project automatically after this time. Use this for ephemeral preview environments or time-boxed jobs.Project lifecycle
Every project moves through a defined set of phases. Phase transitions are driven by two actors: the scheduler (part ofcara-server) and the
agent running on the assigned node.
| Phase | Set by | Meaning |
|---|---|---|
Pending | API server | Project accepted; the scheduler has not yet assigned a node. |
Scheduled | Scheduler | A target node has been chosen and written to status.nodeRef. The agent has not yet confirmed the containers are running. |
Running | Agent | All containers are up and healthy. |
Failed | Agent | The agent could not start the project or reported a terminal error. Check status.conditions for details. |
Terminating | API server | Deletion has been requested. The agent is tearing down containers and Docker resources. |
Terminated | Agent | All containers and Docker resources have been removed. The record is deleted from the store shortly after this phase is observed. |
Conditions
status.conditions is a list of Condition objects that give you structured
detail about what happened at each phase transition. Two condition types are
relevant for projects:
Phase
Phase
Updated on every lifecycle phase transition.
status is always True;
the condition acts as a structured changelog rather than a health signal.
Read reason (a CamelCase word) and message (human-readable) to
understand why the project entered its current phase.NotReadyAt
NotReadyAt
Written once when the control plane first observes a
Running project on
a NotReady node. lastTransitionTime marks the start of the grace
period — after this window the control plane may intervene and reschedule
the project.TerminatingAt
TerminatingAt
Written once when the control plane first observes a
Terminating project
on a NotReady node. lastTransitionTime marks the start of the
force-termination timeout clock.Example: multi-service project
The following manifest deploys a WordPress application with a MySQL database. Theapp service references db by name — this works because both containers
share the project’s Docker bridge network, just like Docker Compose.
Running:
Docker resource naming
The agent uses deterministic names for every Docker resource it creates, so reconciliation remains stateless across restarts.| Resource | Pattern |
|---|---|
| Network | cara-{projectName} |
| Container | {projectName}-{serviceName} |
| Volume | cara-{projectName}-{volumeName} |