Add a Cloud Provider
Caracal’s deployment logic contains no cloud provider. Each deployment model has a provider-neutral core that owns the whole deployment shape, and a small adapter per provider that translates generic concepts into that provider’s resources. Supporting a new cloud, or an internal platform, means writing adapters. It never means changing the core.
What the Core Owns
Section titled “What the Core Owns”The core decides everything that is a property of Caracal rather than of a cloud: which images run, which start command selects a service role, which ports they listen on, which credentials each service needs, how readiness is probed, how far each service scales, and that schema migrations complete before any service rolls.
An adapter never restates these. If you find yourself encoding a port, an image, or a service’s environment in an adapter, that fact belongs in the core.
Deployment Models
Section titled “Deployment Models”| Model | Core | Adapter |
|---|---|---|
| Virtual machine | infra/tofu/modules/caracalHost renders cloud-init | infra/tofu/providers/<provider>/host creates the instance, exposure, identity, and DNS |
| Managed container platform | infra/containerPlatform/topology.yaml and render.mjs | infra/containerPlatform/targets/<provider>.mjs renders platform manifests |
| Kubernetes | infra/helm/caracal | A values overlay in infra/helm/caracal/examples |
Virtual Machine Adapter
Section titled “Virtual Machine Adapter”Implement one OpenTofu module that satisfies the host contract. The contract is the same on every cloud, so a caller swaps providers by changing a module source and nothing else.
Inputs: name, region, machineSize, diskGb, userData, adminUsername, adminPublicKey, ingressCidrs, adminCidrs, networkCidr, dnsZone, hostnames, dnsTtl, tags. Outputs: publicIp, hostId, identityId, hostnames.
Group any input only your provider needs after the shared contract, under a comment naming the provider. Create the instance, its network exposure, a cloud identity, and the DNS records; nothing else. Open inbound 80 alongside 443, because certificate issuance answers its challenge over plain HTTP before a certificate exists.
module "bootstrap" { source = "../../modules/caracalHost" caracalVersion = "v0.2.1" tlsProxy = { email = "ops@example.com" routes = { "console.example.com" = "web" } }}
module "host" { source = "../../providers/myCloud/host" name = "caracal-prod" region = "region-1" userData = module.bootstrap.userData adminPublicKey = file("~/.ssh/id_ed25519.pub") hostnames = ["console.example.com"]}bash infra/tofu/scripts/validate.sh validates every adapter against its real provider schema and fails if any contract input or output is missing.
Managed Container Platform Adapter
Section titled “Managed Container Platform Adapter”Implement one module exporting three members, then register it in render.mjs.
| Member | Purpose |
|---|---|
secretDelivery | file when the platform can project a secret onto a filesystem, env when it can only bind a variable |
internalUrl(service, config) | The address other services reach this one on inside the deployment. Receives { name, port } |
render(plan, config) | Returns a map of file name to file body |
experimental | Set true until the adapter has run against a live account |
secretDelivery is the whole of the secrets abstraction. Every Caracal service accepts a credential either as a variable or through its _FILE form; the core reads your declaration and binds whichever the platform supports. Declare file and the core sets DATABASE_URL_FILE and hands you the list of files to project. Declare env and it sets DATABASE_URL and hands you the secret name to reference. Your adapter never chooses a variable name.
Name rendered files so they sort in apply order, with migration jobs ahead of services: 10-job-migrate, 20-app-sts, and so on. Ship an apply flow that runs the jobs to completion before rolling any service, because no platform sequences that for you.
Add an example deployment config under examples/, then run bash infra/containerPlatform/scripts/validate.sh. It renders every adapter through the same assertions: stock images selected by command, credentials resolved from a secret manager and never materialised, internal services not publicly exposed, and jobs sorted ahead of services.
Kubernetes Adapter
Section titled “Kubernetes Adapter”The chart is already provider-neutral. A cloud is four values:
| Value | Concept |
|---|---|
replayPersistence.storageClassName | Durable per-replica storage |
ingress.*.className and annotations | Ingress and certificates |
serviceAccount.annotations | Keyless cloud identity |
global.podLabels | Any label the provider’s identity webhook requires |
Copy an existing overlay from infra/helm/caracal/examples, substitute those values and the matching External Secrets store, and add it to the loop in the chart’s validation script. A cloud that needs a chart change is a bug in the chart, not in the overlay.
Maturity
Section titled “Maturity”Mark a new adapter experimental until it has been exercised against a live account. For a container platform adapter that is experimental: true, which makes the renderer warn on every run; elsewhere it is a note at the top of the file and a row in Cloud Support Matrix. Rendering cleanly proves a manifest is well formed. It does not prove a deployment works.
Next Step
Section titled “Next Step”Read Cloud Support Matrix for what each provider currently implements.

