Plugin-framework provider covering projects, environments, applications, Compose stacks, managed databases, domains, mounts, ports, redirects, basic auth, registries, SSH keys, certificates and backup destinations, over Dokploy's tRPC-over-REST API. The shim package exposes the provider to other Go modules, which is how pulumi-dokploy bridges it.
441 lines
16 KiB
Markdown
441 lines
16 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
|
|
|
|
The provider is not published to a registry yet. Build it and install it into a
|
|
local filesystem mirror:
|
|
|
|
```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 = "github.com/maxvojtkov/terraform-provider-dokploy//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://github.com/maxvojtkov/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: maxvojtkov/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 the provider binary and use
|
|
it straight away:
|
|
|
|
```bash
|
|
pulumi package add terraform-provider maxvojtkov/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://github.com/maxvojtkov/pulumi-dokploy), which wraps
|
|
this provider and ships SDKs for TypeScript, Python, Go and .NET:
|
|
|
|
```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
|
|
```
|
|
|
|
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.
|