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.
Here’s the path from an empty repo to a provider on the registry, with the lessons where they hurt.
1. Scaffold on the plugin framework
Start from HashiCorp’s scaffolding template and keep the standard shape: 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.
2. 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:
TF_ACC=1 go test ./internal/provider/ -run TestAcc -vMocks 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 a collection 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.
3. Release through the registry
The Terraform registry wants tagged releases with signed artifacts and a manifest; goreleaser produces the whole matrix from one config, and the registry ingests each tag within minutes:
git tag v0.10.0
git push origin v0.10.0 # CI runs goreleaser; the registry picks up the tagAfter that, releasing often is cheap - six of the ten minors shipped in single-day bursts as the resource surface grew. Generate the docs with tfplugindocs so the registry pages and the repo never drift.
4. Hold v1.0 until it earns it
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.