OrbStack Provider is a Terraform provider I wrote from scratch so a homelab has a trail. The first setups that needed it were on OrbStack: Linux machines through orbctl, and containers through the Docker engine it already runs. Published as machugram/orbstack.
resource "orbstack_machine" "dev" {
name = "dev"
image = "ubuntu:noble"
cpus = 2
memory_mib = 2048
disk_gib = 16
}
resource "orbstack_container" "web" {
name = "web"
image = "nginx:latest"
ports = { "8080" = "80" }
}The longer writeup of the lifecycle rules is Two Control Planes on One Mac.
Why I built it
Thinking about a homelab, I realized the setups had no trail. A machine I built last month lived in whatever commands I still remembered. Terraform is the trail: the file is the setup, plan shows the drift, apply rebuilds it, destroy takes it down.
OrbStack was the place that needed that first. The builds on this Mac were a Linux machine with real CPU, memory, and disk, and the containers beside it. Those lived as orbctl and docker commands. I wanted them in the same .tf files as the rest of the lab.
Robert de Bock’s provider was the published OrbStack provider. It creates a machine and reads orb info. CPU, memory, disk, isolation, mounts, and containers sit outside that surface. That was too small for the builds I wanted to record.
I started over. A new provider, with machines that take the limits orbctl already accepts, and one container resource on the OrbStack Docker socket so a web process is a block in the same file as the machine. The general Docker provider can do the container half, and it asks for an image resource, nested port blocks, and a socket. The file I wanted is the short one above.
Tech Stack
- Go — Terraform Plugin Framework provider, module
github.com/machugram/terraform-provider-orbstack - orbctl — Linux machines: create, limits, rename, power, JSON info
- Docker Engine API — containers through
github.com/moby/moby/clienton the OrbStack socket - GoReleaser — a
v*tag builds the signed GitHub release the Terraform Registry publishes
Architecture Overview
Terraform only talks to the plugin. The plugin turns a plan into a request and calls a client. It does not build orbctl arguments or dial Docker itself.
internal/provider— schema, plan, and state fororbstack_machineandorbstack_containerinternal/orb—orbctl: create flags, JSON decoding, limits, cloud-init, rename, powerinternal/docker— image pull, container create, ports, volumes, start and stop
Machine state stores the ULID from orbctl info, so a rename stays an in-place update. Container state stores the engine container id. go test never starts OrbStack.
sequenceDiagram
participant Terraform
participant Provider
participant Client
participant Upstream
Terraform->>Provider: apply
Provider->>Client: CreateMachine or CreateContainer
Client->>Upstream: orbctl, or the Docker engine
Upstream-->>Client: ULID, or container id
Client-->>Provider: record
Provider-->>Terraform: state id
Key Features
- Machines with limits — CPU, memory, disk, isolation, and mounts on
orbstack_machine - One container resource — name an image and create pulls it; no separate image resource
- Honest replace rules — image, cloud-init, and mounts replace a machine; CPU, memory, and disk update in place
- Data sources —
orbstack_machine,orbstack_machines, andorbstack_container - Offline tests — argv, JSON, and port mapping are unit-tested without a running OrbStack
- Registry release — signed checksums; docs generated from the schema with
tfplugindocs
Kubernetes, Compose, image builds, custom networks, and file copy are out of this pass. orbctl push has no delete, so a file resource would create something Terraform cannot destroy.