I wanted terraform apply to be the only deploy button on my
personal infrastructure. Dokploy had an API and no living provider,
so now there is vanillauys/dokploy: written in Go on the
terraform-plugin-framework, at v0.10.0 after ten minor releases.
The shape is the standard one. A thin API client package that knows nothing about Terraform, and a resource layer that maps schema to client calls:
func (r *ProjectResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ProjectModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
project, err := r.client.CreateProject(ctx, plan.Name.ValueString())
// map the response back into state, nothing clever
}
The framework does the heavy lifting: plan diffs, state upgrades, unknown-value propagation. Your job is to be honest about the API you are wrapping.
Test against the real thing
The acceptance suite runs against an actual Dokploy instance in CI on every pull request - a container on the runner’s own Docker, no docker-in-docker. Mocks would have passed builds that failed against the real server, because a young API has opinions the docs do not mention. Every one of these came out of a failing acceptance test, not a bug report:
- Computed collections need
UseStateForUnknown, or every plan recomputes them and downstream references cascade into unknowns. Where the API recomputes them anyway, the answer is a data source and documentation, not wishful state. - Some deletes are asynchronous.
Deletehas to poll until the resource is actually gone, or the next test’sCreatecollides with a name that still exists.
Releasing is a checklist, not a ceremony
The Terraform registry wants tagged releases with signed artifacts and a manifest; goreleaser produces the matrix and the registry ingests each tag within minutes. After that, releasing often is cheap - six of the ten minors shipped in single-day bursts as the resource surface grew.
v1.0 is deliberately not tagged yet. The rule: the provider must survive one Dokploy minor upgrade without breaking changes first. A 1.0 is a promise about the future, and I want at least one data point before making it.