Files
Max Vojtkov c6f1942950
Some checks failed
build / build (push) Successful in 15m46s
release / release (push) Failing after 26m57s
Clarify which Terraform registry Gitea actually has
Gitea lists "Terraform" among its package registries, which reads as if
the provider could be published there. It stores remote state for the
http backend -- not modules, not providers.
2026-08-09 12:49:11 +03:00

477 lines
18 KiB
Markdown

# terraform-provider-dokploy
Manage [Dokploy](https://dokploy.com) as code: projects, environments,
applications, Compose stacks, managed databases, domains, mounts, ports,
redirects, basic auth, registries, SSH keys, certificates and backup
destinations.
Works with Terraform and OpenTofu. Because it is a real Terraform provider
(built on `terraform-plugin-framework`), it can also be
[bridged into Pulumi](#using-this-from-pulumi) without rewriting anything.
```hcl
resource "dokploy_project" "shop" {
name = "shop"
}
resource "dokploy_postgres" "db" {
name = "shop-db"
environment_id = dokploy_project.shop.default_environment_id
docker_image = "postgres:16-alpine"
database_name = "shop"
database_user = "shop"
database_password = var.db_password
}
resource "dokploy_application" "api" {
name = "api"
environment_id = dokploy_project.shop.default_environment_id
source_type = "docker"
docker_image = "ghcr.io/acme/api:1.4.0"
env = "DATABASE_URL=postgresql://shop:${var.db_password}@${dokploy_postgres.db.app_name}:5432/shop"
}
resource "dokploy_domain" "api" {
application_id = dokploy_application.api.id
host = "api.example.com"
port = 3000
https = true
certificate_type = "letsencrypt"
}
```
## Contents
- [Installing](#installing)
- [Configuring](#configuring)
- [How Dokploy's model maps to Terraform](#how-dokploys-model-maps-to-terraform)
- [Resources and data sources](#resources-and-data-sources)
- [The `web-service` module](#the-web-service-module)
- [Deployments are not managed](#deployments-are-not-managed)
- [Importing existing infrastructure](#importing-existing-infrastructure)
- [Using this from Pulumi](#using-this-from-pulumi)
- [Known API quirks](#known-api-quirks)
- [Development](#development)
## Installing
This provider is distributed through
[Gitea releases](https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases)
and installed through a **filesystem mirror**, which is Terraform's supported
way to use a provider that no registry serves.
Gitea *does* list "Terraform" among its package registries, which is
misleading here: that one stores [remote
state](https://docs.gitea.com/usage/packages/terraform) for the `http` backend.
It is not a module registry and not a provider registry, and there is no Gitea
endpoint that can serve the provider protocol `terraform init` speaks.
### From a release
```bash
VERSION=0.1.0
OS_ARCH="$(go env GOOS)_$(go env GOARCH)"
BASE=https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/download
DEST=~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/$VERSION/$OS_ARCH
mkdir -p "$DEST"
curl -fsSLO "$BASE/v$VERSION/terraform-provider-dokploy_${VERSION}_${OS_ARCH}.zip"
unzip -j "terraform-provider-dokploy_${VERSION}_${OS_ARCH}.zip" -d "$DEST"
```
Each release also carries a `_SHA256SUMS` file, worth checking before you
unzip. There are no GPG signatures: those are required by the registry
protocol, and a filesystem mirror never verifies them, so publishing one would
imply a check that nothing performs.
### From source
```bash
make install
```
That writes the binary to
`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.1.0/<os>_<arch>/`
and prints the CLI configuration to add to `~/.terraformrc`:
```hcl
provider_installation {
filesystem_mirror {
path = "/Users/you/.local/share/terraform/plugins"
include = ["registry.terraform.io/maxvojtkov/dokploy"]
}
direct {
exclude = ["registry.terraform.io/maxvojtkov/dokploy"]
}
}
```
Then in your configuration:
```hcl
terraform {
required_providers {
dokploy = {
source = "maxvojtkov/dokploy"
version = "0.1.0"
}
}
}
```
## Configuring
Generate an API token in Dokploy under **Settings → Profile → API/CLI**.
```hcl
provider "dokploy" {
host = "https://dokploy.example.com"
api_key = var.dokploy_api_key
}
```
Both are better supplied through the environment, which keeps the token out of
your configuration entirely:
| Attribute | Environment variable | Default |
| ---------------------- | ------------------------- | ------- |
| `host` | `DOKPLOY_HOST` | — |
| `api_key` | `DOKPLOY_API_KEY` | — |
| `timeout_seconds` | `DOKPLOY_TIMEOUT_SECONDS` | `60` |
| `insecure_skip_verify` | — | `false` |
```bash
export DOKPLOY_HOST=https://dokploy.example.com
export DOKPLOY_API_KEY=...
terraform apply
```
## How Dokploy's model maps to Terraform
Dokploy nests everything under a project:
```
project
└── environment (a default "production" one is created for you)
├── application ├── domain, mount, port, redirect, security
├── compose └── domain, mount
└── postgres | mysql | mariadb | mongo | redis
```
Two consequences worth knowing up front:
**Every project comes with a default environment.** Creating a
`dokploy_project` also creates a `production` environment server-side. Rather
than making you import it, the provider exposes it as
`default_environment_id`:
```hcl
resource "dokploy_project" "shop" {
name = "shop"
}
resource "dokploy_application" "api" {
environment_id = dokploy_project.shop.default_environment_id
# ...
}
```
Declare `dokploy_environment` only for *additional* environments such as
staging. Declaring one named `production` would create a second environment
with the same name, not adopt the existing one.
**Services attach to environments, not projects.** `environment_id` is always
the parent reference, and changing it forces replacement — Dokploy moves
services through a separate `move` endpoint that has no declarative equivalent.
## Resources and data sources
Full reference documentation lives in [`docs/`](./docs).
### Resources
| Resource | Purpose |
| -------------------------------------------------------- | ------------------------------------------------ |
| [`dokploy_project`](docs/resources/project.md) | Top-level container |
| [`dokploy_environment`](docs/resources/environment.md) | Additional environments within a project |
| [`dokploy_application`](docs/resources/application.md) | A service built from Git, a Docker image or an upload |
| [`dokploy_compose`](docs/resources/compose.md) | A Docker Compose or Swarm stack |
| [`dokploy_postgres`](docs/resources/postgres.md) | Managed PostgreSQL |
| [`dokploy_mysql`](docs/resources/mysql.md) | Managed MySQL |
| [`dokploy_mariadb`](docs/resources/mariadb.md) | Managed MariaDB |
| [`dokploy_mongo`](docs/resources/mongo.md) | Managed MongoDB |
| [`dokploy_redis`](docs/resources/redis.md) | Managed Redis |
| [`dokploy_domain`](docs/resources/domain.md) | A hostname routed through Traefik |
| [`dokploy_mount`](docs/resources/mount.md) | Volume, bind mount or config file |
| [`dokploy_port`](docs/resources/port.md) | A port published straight onto the host |
| [`dokploy_redirect`](docs/resources/redirect.md) | A Traefik redirect rule |
| [`dokploy_security`](docs/resources/security.md) | HTTP basic auth credentials |
| [`dokploy_registry`](docs/resources/registry.md) | A container registry |
| [`dokploy_ssh_key`](docs/resources/ssh_key.md) | SSH key pair for private Git and remote servers |
| [`dokploy_certificate`](docs/resources/certificate.md) | An uploaded TLS certificate |
| [`dokploy_destination`](docs/resources/destination.md) | S3-compatible backup destination |
### Data sources
| Data source | Purpose |
| ------------------------------------------------------------ | ---------------------------------------- |
| [`dokploy_project`](docs/data-sources/project.md) | Look up a project and its environments |
| [`dokploy_projects`](docs/data-sources/projects.md) | List every visible project |
| [`dokploy_environment`](docs/data-sources/environment.md) | Look up a single environment |
| [`dokploy_application`](docs/data-sources/application.md) | Look up a single application |
| [`dokploy_servers`](docs/data-sources/servers.md) | List registered remote servers |
## The `web-service` module
[`modules/web-service`](./modules/web-service) bundles the pieces a typical
public service needs — the application plus its domains, ports, mounts, basic
auth and redirects — behind one call:
```hcl
module "storefront" {
source = "git::https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy.git//modules/web-service"
name = "storefront"
environment_id = dokploy_project.shop.default_environment_id
service_source = {
type = "github"
github_id = var.github_provider_id
owner = "acme"
repository = "storefront"
branch = "main"
}
replicas = 2
resources = { memory_limit = "1g", cpu_limit = "1" }
domains = [
{ host = "shop.example.com", port = 3000 },
]
mounts = [
{ mount_path = "/app/uploads", type = "volume", volume_name = "uploads" },
]
}
```
The variable is `service_source` rather than `source` because `source` is a
reserved module meta-argument in Terraform.
See [`examples/complete`](./examples/complete) for a full environment: project,
staging environment, Postgres, Redis, two services and a Compose stack.
## Deployments are not managed
This provider manages *configuration*, not *rollouts*. Creating or updating a
`dokploy_application` writes its definition; it does not build or start
anything. That is deliberate — a deployment is an imperative, time-bounded
action with build logs and failure modes, not a converged state Terraform can
own.
Trigger deployments from the Dokploy UI, from CI, or from the API:
```bash
curl -X POST "$DOKPLOY_HOST/api/application.deploy" \
-H "x-api-key: $DOKPLOY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"applicationId":"'"$APP_ID"'"}'
```
### From CI
[`dokploy-deploy-action`](https://gitea.coolify.vojtkov.dev/usr_unknown/dokploy-deploy-action)
is the same call with the parts that matter in a pipeline: it waits for the
build, fails the job when the deployment fails, and prints the build log.
It runs on GitHub Actions and Gitea Actions.
```yaml
- uses: https://gitea.coolify.vojtkov.dev/usr_unknown/dokploy-deploy-action@v1
with:
host: ${{ secrets.DOKPLOY_HOST }}
api-key: ${{ secrets.DOKPLOY_API_KEY }}
application-id: ${{ needs.infra.outputs.api_application_id }}
docker-image: ghcr.io/acme/api:${{ github.sha }}
```
Feed it this provider's `id` output and the division of labour stays clean:
Terraform owns the application's configuration, the action deploys code into
it.
### From Terraform itself
To couple the two, hang a `terraform_data` resource off the application so a
configuration change triggers a redeploy:
```hcl
resource "terraform_data" "deploy_api" {
triggers_replace = [
dokploy_application.api.docker_image,
dokploy_application.api.env,
]
provisioner "local-exec" {
command = <<-CMD
curl -fsS -X POST "$DOKPLOY_HOST/api/application.deploy" \
-H "x-api-key: $DOKPLOY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"applicationId":"${dokploy_application.api.id}"}'
CMD
}
}
```
Most teams are better served letting Dokploy's own Git triggers
(`auto_deploy = true`) handle rollouts and keeping Terraform to configuration.
## Importing existing infrastructure
Every resource imports by its Dokploy ID:
```bash
terraform import dokploy_project.shop xK2bZpaQQH4XDEmz9CfX1
terraform import dokploy_application.api w_CJkBMFJTmkHZHVbXrJI
terraform import dokploy_postgres.db H9o0itXlfPk8wxrRfIb8m
```
Find IDs through the API:
```bash
curl -H "x-api-key: $DOKPLOY_API_KEY" "$DOKPLOY_HOST/api/project.all" \
| jq -r '.[] | "\(.name)\t\(.projectId)"'
```
`dokploy_mount` is the one exception: Dokploy stores the parent as a typed
column (`applicationId`, `composeId`, …) and never echoes back the generic
`serviceId` that creation uses, so set `service_id` and `service_type` in your
configuration to match reality after importing.
## Using this from Pulumi
Because this is a standard Terraform provider, Pulumi consumes it through
[`pulumi-terraform-bridge`](https://github.com/pulumi/pulumi-terraform-bridge)
— no reimplementation needed. Two routes:
**Dynamic bridge (no build step).** Point Pulumi at a provider binary and use
it straight away. The usual `owner/name` shorthand resolves through the
Terraform registry, which does not serve this provider, so give a path
instead — `make build` produces one:
```bash
pulumi package add terraform-provider ./terraform-provider-dokploy
```
Pulumi generates an SDK for your language on the spot. This is the fastest path
and keeps you in lockstep with the Terraform provider.
**Static bridge (a published SDK).** For a first-class, versioned package with
richer types, use
[`pulumi-dokploy`](https://gitea.coolify.vojtkov.dev/usr_unknown/pulumi-dokploy), which wraps
this provider and ships SDKs for TypeScript, Python, Go and .NET from this
Gitea instance's package registries:
```bash
npm install @maxvojtkov/pulumi-dokploy
pip install pulumi_dokploy
go get github.com/maxvojtkov/pulumi-dokploy/sdk/go/dokploy
dotnet add package Maxvojtkov.Dokploy
```
Each of those needs its registry pointed at Gitea first — the one-time
configuration per language is in that repository's
[Installing](https://gitea.coolify.vojtkov.dev/usr_unknown/pulumi-dokploy#installing)
section.
That repository holds nothing but the mapping — every resource, schema and API
call still comes from here, so the two providers move together.
Three things are worth knowing about the translation:
- Attribute names become camelCase in Pulumi (`environment_id`
`environmentId`, `default_environment_id``defaultEnvironmentId`).
- `name` is auto-generated when omitted, following Pulumi convention. Set it
explicitly when the Dokploy-side name matters.
- The `web-service` Terraform module has no automatic Pulumi equivalent —
reimplement it as a
[ComponentResource](https://www.pulumi.com/docs/concepts/resources/components/),
which is a natural fit for the same grouping. `pulumi-dokploy` ships a port
of it in `examples/typescript/webService.ts`.
### The `shim` package
`internal/provider` cannot be imported from another Go module, so the bridge
goes through the small `shim` package instead:
```go
import tfshim "github.com/maxvojtkov/terraform-provider-dokploy/shim"
p := tfshim.NewProvider("0.1.0") // a plugin-framework provider.Provider
```
Keep `shim.NewProvider` stable — it is this repository's only public Go API.
## Known API quirks
These are properties of the Dokploy API that the provider works around; they
explain behaviour that would otherwise look surprising.
- **`create` accepts only a subset of fields.** For applications, Compose
stacks, environments and all five databases, Dokploy's create endpoints
ignore most fields. The provider creates the resource and immediately issues
an update with the rest, so a single `apply` still converges.
- **Some endpoints return no ID.** `sshKey.create`, `redirects.create` and
`security.create` return nothing or `true`. The provider identifies the new
record by diffing the relevant list before and after the call. If several are
created outside Terraform at the same moment, this is ambiguous and the
provider reports an error rather than guessing.
- **`registry.create` performs a real `docker login`.** Invalid credentials
fail the apply with the Docker error, by design.
- **Creating a registry, SSH key or certificate needs an organization.** The
provider resolves it once per run from `user.get`.
- **`null` is not universally accepted.** Endpoints generated from the database
schema accept `null` to clear a nullable column; hand-written ones (such as
`environment.create`) reject it. The provider tracks this per field.
- **Basic auth passwords are never returned.** `dokploy_security.password`
keeps whatever you configured; drift in that one field cannot be detected.
## Development
```bash
make build # compile
make test # unit tests
make testacc # acceptance tests (creates and destroys real resources)
make docs # regenerate docs/ from the provider schema
make fmt lint # gofmt + go vet + terraform fmt
make install # install into the local filesystem mirror
```
Acceptance tests talk to a live Dokploy instance and create resources prefixed
`tfacc-`, destroying them afterwards. Point them at a scratch instance:
```bash
TF_ACC=1 \
DOKPLOY_HOST=https://dokploy.example.com \
DOKPLOY_API_KEY=... \
make testacc
```
They require the `terraform` binary; `terraform-plugin-testing` does not fully
support OpenTofu's provider addressing. The provider itself works with both.
### Layout
```
internal/client/ HTTP client for /api/<router>.<procedure>
internal/tfmap/ reflection-based mapping between models and Dokploy JSON
internal/provider/ provider, resources, data sources
modules/web-service/ reusable module
examples/complete/ worked example
docs/ generated reference documentation
```
Resources are declared as `ResourceSpec` values — the procedures to call plus a
model struct whose `dokploy:"..."` tags describe how each field is serialized —
and a single generic implementation provides CRUD and import for all of them.
Adding a resource means writing a model and a spec, not another CRUD loop.