4 Commits
Author SHA1 Message Date
max-voitcov 2d1caf6e73 Cover what Dokploy v0.30 added
build / build (push) Successful in 3m56s
release / release (push) Successful in 15m2s
Six new resources, all backed by endpoints that did not exist before v0.30.0
and verified end-to-end against a live v0.30.2 instance:

  dokploy_network         Docker networks, now first-class. Services attach
                          through network_ids, which is what deprecates
                          Compose's isolated_deployment upstream.
  dokploy_dns_provider    Cloudflare or Route53, so adding a domain creates
                          its DNS record.
  dokploy_vault_provider  Env values resolved from HashiCorp Vault, Infisical,
                          AWS, Doppler, Azure or Scaleway at deploy time, so
                          the secret never lands in Dokploy or in state.
  dokploy_schedule        Cron jobs in a container, a stack, or on a server.
  dokploy_volume_backup   Scheduled backups of a named volume — the companion
                          to a mount that persists.
  dokploy_libsql          The sixth managed database engine.

libsql.create is the strictest endpoint in the API: eleven keys required to
be present, several only meaningfully null, no generated service name, and it
returns `true` rather than the row. CreateDefaults and ListIDs absorb all
three so the resource behaves like every other database.

Also filled the gaps a field-by-field diff against the live schema turned up:
domain gains `enabled` (the v0.30.0 park-a-domain toggle), compose gains
create_env_file, icon and service_networks, application gains icon and
preview_require_collaborator_permissions, and mounts accept libsql.

DNS and vault credentials are masked by Dokploy on read, so `config` is
tagged noread and keeps the configured value, as the basic-auth password
already does.
2026-08-26 00:40:27 +03:00
max-voitcov 3ddce62647 Reject volume mounts that silently never persist
A `dokploy_mount` with `type = "volume"` and no `volume_name` was accepted
by both this provider and Dokploy. Dokploy renders the mount as
`{Source: volumeName || "", Target: mountPath}`, and Docker reads an empty
source as an anonymous volume: every deploy created a fresh one and orphaned
the last, so the data never survived a redeploy while disk usage climbed.
Nothing errored at any point, which is what made it worth catching here.

The pairing is now checked at plan time, before anything is created, and the
error explains the consequence rather than only the rule. The same validator
covers `bind` without `host_path` and `file` without `file_path`, and rejects
a field set against the wrong type, which Dokploy would otherwise ignore.

Verified against a live v0.30.2 instance: the offending config plans cleanly
before the change and is refused after it.
2026-08-26 00:40:03 +03:00
max-voitcov 0890384552 Add ConfigValidators and CreateDefaults hooks to the generic resource
Two things ResourceSpec could not express: plan-time checks that span
several attributes, and create bodies that need a key Dokploy insists on
receiving but the model cannot produce. Both are used by the commits that
follow.
2026-08-26 00:39:51 +03:00
usr_unknown 209079cf68 Stop paying the unreachable cache server's timeout
build / build (push) Successful in 2m37s
release / release (push) Successful in 9m29s
setup-go caches by default. act_runner's built-in cache server binds to
an address job containers cannot route to, so every restore and save
blocks until it times out:

  Failed to restore: getCacheEntry failed: connect ETIMEDOUT 10.0.2.12:40789
  Failed to save:    reserveCache failed: connect ETIMEDOUT 10.0.2.12:40789

That was 634s of a 946s build. Disable caching until the runner's
cache.host points somewhere reachable, then turn it back on.
2026-08-09 13:57:18 +03:00
29 changed files with 1554 additions and 18 deletions
+5 -1
View File
@@ -18,7 +18,11 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: go.sum
# setup-go caches by default, and this runner's built-in cache server
# is unreachable from job containers -- every restore and save hangs
# until it times out, which cost 634s of a 946s build. Re-enable once
# act_runner's cache.host points somewhere job containers can route.
cache: false
- name: Format
run: test -z "$(gofmt -l .)" || { gofmt -l .; echo "run gofmt -w ."; exit 1; }
+3
View File
@@ -17,6 +17,9 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: "1.25.x"
# See build.yml: the runner's cache server is unreachable from job
# containers, so caching costs minutes of timeouts instead of saving.
cache: false
- uses: goreleaser/goreleaser-action@v6
with:
+92
View File
@@ -0,0 +1,92 @@
# Changelog
All notable changes to this provider are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] — 2026-08-26
Tracks **Dokploy v0.30.x**. Verified against a live v0.30.2 instance
(597 API operations).
### Fixed
- **`dokploy_mount` could silently create a volume that never persisted
anything.** A `type = "volume"` mount with no `volume_name` was accepted by
Dokploy and by this provider. Dokploy renders such a mount as
`{Source: "", Target: <mount_path>}`, and Docker reads an empty source as an
*anonymous* volume: every deploy created a brand-new volume and orphaned the
previous one, so data never survived a redeploy and disk usage grew on every
deployment.
The provider now rejects that configuration at **plan** time, before anything
is created, with an error explaining the consequence. The same check covers
the two neighbouring cases — `type = "bind"` without `host_path` and
`type = "file"` without `file_path` — and flags a field set for the wrong
`type`, which Dokploy would otherwise ignore silently.
If you already have such a mount, add a `volume_name`, apply, and redeploy
the service. The data in the current anonymous volume is **not** migrated;
copy it off first if you need it.
### Added
New resources, all from Dokploy v0.30.0:
- **`dokploy_network`** — Docker networks as first-class objects: `bridge` or
`overlay`, with `internal`, `attachable`, IPv4/IPv6, `mtu` and custom `ipam`.
Docker networks are immutable, so every attribute forces replacement.
- **`dokploy_dns_provider`** — a Cloudflare or AWS Route53 connection that lets
Dokploy create DNS records for domains automatically.
- **`dokploy_vault_provider`** — resolve environment variables from an external
secret manager at deploy time with `${{vault.<scope>.<key>}}`. Supports
HashiCorp Vault/OpenBao, Infisical, AWS Secrets Manager, Doppler, Azure Key
Vault and Scaleway.
- **`dokploy_schedule`** — cron jobs running in an application container, a
Compose service, a remote server, or on the Dokploy host.
- **`dokploy_volume_backup`** — scheduled backups of a Docker volume to a
`dokploy_destination`. The natural companion to a named `dokploy_mount`.
- **`dokploy_libsql`** — the sixth managed database engine, with `primary` and
`replica` `sqld` nodes and libSQL namespaces.
New attributes on existing resources:
- `dokploy_domain` gains **`enabled`**, the v0.30.0 enable/disable toggle. It
pulls a route out of Traefik while keeping certificates, paths and middleware
intact, so a domain can be parked and restored without reconfiguring it.
- `dokploy_compose` gains `create_env_file`, `icon` and **`service_networks`**,
which attaches individual services in a stack to specific Docker networks.
- `dokploy_application` gains `icon` and
`preview_require_collaborator_permissions`.
- `dokploy_mount` and `dokploy_volume_backup` accept `libsql` as a
`service_type`.
### Changed
- `network_ids` and `detach_dokploy_network` are now documented as the
supported way to control service networking. Compose's
`isolated_deployment` is **deprecated upstream** in v0.30.0 — attaching
networks per service covers the same ground and survives restarts. The
attribute still exists and still works; prefer `network_ids` for new
configurations.
### Notes
- Dokploy's `libsql.create` requires eleven keys to be present even when null
is the only sensible value, and does not generate a service name. The
provider fills both in, so `app_name` stays optional as it is for every other
database.
- `config` on `dokploy_dns_provider` and `dokploy_vault_provider` is masked by
Dokploy on read. Terraform keeps the value you configured and does **not**
detect drift in those credentials.
## [0.1.0] — 2026-08-09
Initial release. Projects, environments, applications, Compose stacks, five
managed databases (PostgreSQL, MySQL, MariaDB, MongoDB, Redis), domains,
mounts, ports, redirects, basic auth, registries, SSH keys, certificates and
backup destinations, plus five data sources and a `web-service` module.
[0.2.0]: https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/tag/v0.2.0
[0.1.0]: https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/tag/v0.1.0
+1 -1
View File
@@ -1,5 +1,5 @@
BINARY := terraform-provider-dokploy
VERSION ?= 0.1.0
VERSION ?= 0.2.0
NAMESPACE := maxvojtkov
NAME := dokploy
+77 -3
View File
@@ -51,6 +51,7 @@ resource "dokploy_domain" "api" {
- [Deployments are not managed](#deployments-are-not-managed)
- [Importing existing infrastructure](#importing-existing-infrastructure)
- [Using this from Pulumi](#using-this-from-pulumi)
- [Volumes that actually persist](#volumes-that-actually-persist)
- [Known API quirks](#known-api-quirks)
- [Development](#development)
@@ -70,7 +71,7 @@ endpoint that can serve the provider protocol `terraform init` speaks.
### From a release
```bash
VERSION=0.1.0
VERSION=0.2.0
OS_ARCH="$(go env GOOS)_$(go env GOARCH)"
BASE=https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/download
@@ -92,7 +93,7 @@ make install
```
That writes the binary to
`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.1.0/<os>_<arch>/`
`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.2.0/<os>_<arch>/`
and prints the CLI configuration to add to `~/.terraformrc`:
```hcl
@@ -114,7 +115,7 @@ terraform {
required_providers {
dokploy = {
source = "maxvojtkov/dokploy"
version = "0.1.0"
version = "0.2.0"
}
}
}
@@ -202,6 +203,7 @@ Full reference documentation lives in [`docs/`](./docs).
| [`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_libsql`](docs/resources/libsql.md) | Managed libSQL (`sqld`) |
| [`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 |
@@ -211,6 +213,11 @@ Full reference documentation lives in [`docs/`](./docs).
| [`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 |
| [`dokploy_network`](docs/resources/network.md) | A Docker network services attach to |
| [`dokploy_schedule`](docs/resources/schedule.md) | A cron job run in a container or on a server |
| [`dokploy_volume_backup`](docs/resources/volume_backup.md) | A scheduled backup of a Docker volume |
| [`dokploy_dns_provider`](docs/resources/dns_provider.md) | Cloudflare or Route53, for automatic DNS records |
| [`dokploy_vault_provider`](docs/resources/vault_provider.md) | An external secret manager for deploy-time env |
### Data sources
@@ -411,6 +418,56 @@ p := tfshim.NewProvider("0.1.0") // a plugin-framework provider.Provider
Keep `shim.NewProvider` stable — it is this repository's only public Go API.
## Volumes that actually persist
A `dokploy_mount` with `type = "volume"` **must** set `volume_name`:
```hcl
resource "dokploy_mount" "data" {
type = "volume"
volume_name = "shop-uploads" # required — see below
mount_path = "/app/uploads"
service_type = "application"
service_id = dokploy_application.api.id
}
```
Dokploy turns a mount into a Docker mount with
`{Source: volumeName || "", Target: mountPath}`. When `volumeName` is null the
source is the empty string, and Docker reads an empty source as an *anonymous*
volume — a new one on every single deploy, with the previous one left orphaned
on disk. Nothing errors: the service starts, the path is writable, and the data
is gone again after the next deployment while disk usage climbs.
Dokploy's API accepts that mount without complaint, so this provider rejects it
at plan time instead:
```
Error: Missing volume_name
with dokploy_mount.data,
on main.tf line 1, in resource "dokploy_mount" "data":
`volume_name` must be set to a non-empty value when `type` is `volume`.
Dokploy passes an unset `volume_name` to Docker as an empty source, which
creates a new anonymous volume on every deploy. The data written to the
previous volume is orphaned and never reused, so the mount silently does not
persist anything.
```
The same check covers `type = "bind"` without `host_path` and `type = "file"`
without `file_path`, and it rejects a field set against the wrong `type` —
`volume_name` on a bind mount, say — which Dokploy would otherwise ignore.
**If you already have such a mount**, adding `volume_name` and redeploying
gives you a persistent volume from that point on. The contents of the current
anonymous volume are not migrated; copy the data off the host first if you
need it.
Pair a named volume with [`dokploy_volume_backup`](docs/resources/volume_backup.md)
to get it off the host on a schedule.
## Known API quirks
These are properties of the Dokploy API that the provider works around; they
@@ -434,6 +491,23 @@ explain behaviour that would otherwise look surprising.
`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.
DNS and vault provider credentials behave the same way: Dokploy masks
`config` on read, so `dokploy_dns_provider.config` and
`dokploy_vault_provider.config` keep the configured value and are not
checked for drift.
- **`libsql.create` is the strictest endpoint in the API.** It requires eleven
keys to be *present* — several only meaningfully `null` — declines to
generate a service name the way every other engine does, and then returns
`true` instead of the created row. The provider fills in the nulls, derives
an `app_name` from `name`, and finds the new ID by diffing the environment's
libSQL list, so `dokploy_libsql` behaves like the other databases.
- **A `volume` mount with no name is silently anonymous.** Covered in
[Volumes that actually persist](#volumes-that-actually-persist); the provider
rejects it at plan time.
- **Docker networks cannot be updated.** Dokploy exposes no `network.update`,
matching Docker itself, so every attribute of `dokploy_network` forces
replacement. Replacing a network detaches the services using it until they
are redeployed.
## Development
+2 -2
View File
@@ -2,12 +2,12 @@
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy Provider"
description: |-
Manage Dokploy https://dokploy.com projects, environments, applications, compose stacks, databases and networking with Terraform.
Manage Dokploy https://dokploy.com projects, environments, applications, compose stacks, databases, Docker networks and networking with Terraform.
---
# dokploy Provider
Manage [Dokploy](https://dokploy.com) projects, environments, applications, compose stacks, databases and networking with Terraform.
Manage [Dokploy](https://dokploy.com) projects, environments, applications, compose stacks, databases, Docker networks and networking with Terraform.
+2
View File
@@ -78,6 +78,7 @@ Dokploy's `application.create` endpoint accepts only a handful of fields, so thi
- `gitlab_repository` (String) GitLab repository name.
- `health_check_swarm` (String) Docker Swarm health check configuration, as a JSON object.
- `heroku_version` (String) Heroku buildpack stack version.
- `icon` (String) Icon shown next to the service in the Dokploy UI.
- `is_preview_deployments_active` (Boolean) Build a preview deployment for each pull request.
- `is_static_spa` (Boolean) Serve a static build as a single-page application.
- `labels_swarm` (String) Docker Swarm service labels, as a JSON object.
@@ -99,6 +100,7 @@ Dokploy's `application.create` endpoint accepts only a handful of fields, so thi
- `preview_limit` (Number) Maximum number of concurrent preview deployments.
- `preview_path` (String) Base path for preview deployments.
- `preview_port` (Number) Container port exposed by preview deployments.
- `preview_require_collaborator_permissions` (Boolean) Only build previews for pull requests opened by users with repository collaborator permissions.
- `preview_wildcard` (String) Wildcard domain used to expose preview deployments.
- `publish_directory` (String) Directory served when `build_type` is `static`.
- `railpack_version` (String) Railpack version.
+3
View File
@@ -40,6 +40,7 @@ Set `compose_file` to manage the stack definition inline (with `source_type = "r
- `compose_file` (String) Inline Compose file contents. Used when `source_type` is `raw`.
- `compose_path` (String) Path to the Compose file within the repository.
- `compose_type` (String) Whether to run the stack with Docker Compose or Docker Swarm. Valid values: `docker-compose`, `stack`.
- `create_env_file` (Boolean) Write the environment variables to a `.env` file next to the Compose file.
- `custom_git_branch` (String) Branch to deploy for a custom Git remote.
- `custom_git_ssh_key_id` (String) SSH key used to clone a private custom Git remote.
- `custom_git_url` (String) Git remote URL, when `source_type` is `git`.
@@ -58,12 +59,14 @@ Set `compose_file` to manage the stack definition inline (with `source_type = "r
- `gitlab_path_namespace` (String) Full GitLab namespace path.
- `gitlab_project_id` (Number) Numeric GitLab project ID.
- `gitlab_repository` (String) GitLab repository name.
- `icon` (String) Icon shown next to the stack in the Dokploy UI.
- `isolated_deployment` (Boolean) Run the stack on its own isolated Docker network.
- `isolated_deployments_volume` (Boolean) Prefix volume names for isolated deployments. Retained for backwards compatibility.
- `owner` (String) GitHub repository owner.
- `randomize` (Boolean) Append a random suffix to service and volume names.
- `repository` (String) GitHub repository name.
- `server_id` (String) Remote server to deploy on. Omit to use the Dokploy host itself.
- `service_networks` (String) Per-service Docker network attachments, as a JSON object mapping each service name in the stack to an array of network IDs.
- `source_type` (String) Where the Compose file comes from. Valid values: `git`, `github`, `gitlab`, `bitbucket`, `gitea`, `raw`.
- `suffix` (String) Suffix appended to generated resource names.
- `trigger_type` (String) What triggers an automatic deployment. Valid values: `push`, `tag`.
+57
View File
@@ -0,0 +1,57 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_dns_provider Resource - dokploy"
subcategory: ""
description: |-
A DNS provider connection Dokploy uses to create records for domains automatically.
config is a JSON object whose shape depends on providerType:
# Cloudflare
config = jsonencode({ providerType = "cloudflare", apiToken = var.cloudflare_token })
# AWS Route53
config = jsonencode({
providerType = "route53"
accessKeyId = var.aws_access_key_id
secretAccessKey = var.aws_secret_access_key
})
~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in config is not detected.
---
# dokploy_dns_provider (Resource)
A DNS provider connection Dokploy uses to create records for domains automatically.
`config` is a JSON object whose shape depends on `providerType`:
```hcl
# Cloudflare
config = jsonencode({ providerType = "cloudflare", apiToken = var.cloudflare_token })
# AWS Route53
config = jsonencode({
providerType = "route53"
accessKeyId = var.aws_access_key_id
secretAccessKey = var.aws_secret_access_key
})
```
~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in `config` is not detected.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `config` (String, Sensitive) Provider credentials as a JSON object, including the `providerType` discriminator.
- `name` (String) Name of the connection. Must be unique within the organization and may contain only letters, digits, `-` and `_`.
### Read-Only
- `created_at` (String) RFC 3339 timestamp of when the connection was created.
- `id` (String) Unique DNS provider identifier.
- `organization_id` (String) Organization that owns the connection.
- `provider_type` (String) Provider kind derived from `config`: `cloudflare` or `route53`.
+1
View File
@@ -30,6 +30,7 @@ Set exactly one of `application_id` or `compose_id`. When targeting a Compose st
- `custom_cert_resolver` (String) Traefik certificate resolver name, when `certificate_type` is `custom`.
- `custom_entrypoint` (String) Traefik entrypoint to bind, when not using the defaults.
- `domain_type` (String) What kind of target this domain points at. Valid values: `compose`, `application`, `preview`.
- `enabled` (Boolean) Whether the domain is served. Setting this to `false` removes the route from Traefik but keeps the certificate, path and middleware configuration intact, so the domain can be brought back without reconfiguring it.
- `forward_auth_enabled` (Boolean) Protect this domain with Dokploy's forward auth.
- `https` (Boolean) Serve the domain over HTTPS and redirect HTTP traffic to it.
- `internal_path` (String) Path the request is rewritten to before it reaches the container, defaults to `/`.
+70
View File
@@ -0,0 +1,70 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_libsql Resource - dokploy"
subcategory: ""
description: |-
A managed libSQL instance running on Dokploy.
~> Creating this resource provisions the service definition but does not start a deployment. Deploy it from the Dokploy UI or CLI.
~> Credentials are stored in Terraform state. Use a state backend with encryption at rest.
libSQL runs as a sqld server. A primary node owns the data; a replica node follows a primary named by sqld_primary_url.
---
# dokploy_libsql (Resource)
A managed libSQL instance running on Dokploy.
~> Creating this resource provisions the service definition but does **not** start a deployment. Deploy it from the Dokploy UI or CLI.
~> Credentials are stored in Terraform state. Use a state backend with encryption at rest.
libSQL runs as a `sqld` server. A `primary` node owns the data; a `replica` node follows a primary named by `sqld_primary_url`.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `database_password` (String, Sensitive) Password for the database user.
- `database_user` (String) Database user to create.
- `docker_image` (String) libSQL server image to run, for example `ghcr.io/tursodatabase/libsql-server:latest`.
- `environment_id` (String) Environment this database belongs to.
- `name` (String) Display name of the database.
### Optional
- `app_name` (String) Unique Docker service name. Generated from `name` when omitted, because Dokploy's libSQL endpoint does not generate one. Changing it forces a new database.
- `command` (String) Override the container entrypoint command.
- `cpu_limit` (String) Hard CPU limit, for example `1`.
- `cpu_reservation` (String) Soft CPU reservation, for example `0.5`.
- `description` (String) Free-form description.
- `detach_dokploy_network` (Boolean) Detach the service from the shared `dokploy-network`.
- `enable_namespaces` (Boolean) Serve multiple logical databases from one instance through libSQL namespaces.
- `endpoint_spec_swarm` (String) Docker Swarm endpoint specification, as a JSON object.
- `env` (String) Environment variables in `KEY=value` format, one per line.
- `external_admin_port` (Number) Host port exposing the admin API.
- `external_grpc_port` (Number) Host port exposing the gRPC replication endpoint.
- `external_port` (Number) Host port exposing the HTTP API.
- `health_check_swarm` (String) Docker Swarm health check configuration, as a JSON object.
- `labels_swarm` (String) Docker Swarm service labels, as a JSON object.
- `memory_limit` (String) Hard memory limit, for example `512m`.
- `memory_reservation` (String) Soft memory reservation, for example `256m`.
- `mode_swarm` (String) Docker Swarm service mode, as a JSON object.
- `network_ids` (List of String) IDs of additional Docker networks to attach.
- `network_swarm` (String) Docker Swarm network attachments, as a JSON array.
- `placement_swarm` (String) Docker Swarm placement constraints, as a JSON object.
- `replicas` (Number) Number of replicas to run.
- `restart_policy_swarm` (String) Docker Swarm restart policy, as a JSON object.
- `rollback_config_swarm` (String) Docker Swarm rollback configuration, as a JSON object.
- `server_id` (String) Remote server to deploy on. Omit to use the Dokploy host itself.
- `sqld_node` (String) Role this node plays in a libSQL cluster. Valid values: `primary`, `replica`. Defaults to `primary`.
- `sqld_primary_url` (String) URL of the primary node, when `sqld_node` is `replica`.
- `stop_grace_period_swarm` (Number) Grace period in nanoseconds before a container is killed.
- `update_config_swarm` (String) Docker Swarm rolling update configuration, as a JSON object.
### Read-Only
- `application_status` (String) Current status reported by Dokploy: `idle`, `running`, `done` or `error`.
- `created_at` (String) RFC 3339 timestamp of when the database was created.
- `id` (String) Unique libSQL identifier.
+4 -1
View File
@@ -5,6 +5,7 @@ subcategory: ""
description: |-
A volume, bind mount, or config file attached to a Dokploy service.
type = "volume" — a named Docker volume; set volume_name.type = "bind" — a path on the host; set host_path.type = "file" — a file rendered from content; set file_path.
~> A volume mount must set volume_name. Dokploy hands an unset name to Docker as an empty source, which creates a fresh anonymous volume on every deploy and orphans the previous one — the data never survives a redeploy. The provider rejects that combination at plan time.
---
# dokploy_mount (Resource)
@@ -15,6 +16,8 @@ A volume, bind mount, or config file attached to a Dokploy service.
* `type = "bind"` — a path on the host; set `host_path`.
* `type = "file"` — a file rendered from `content`; set `file_path`.
~> **A `volume` mount must set `volume_name`.** Dokploy hands an unset name to Docker as an empty source, which creates a fresh anonymous volume on every deploy and orphans the previous one — the data never survives a redeploy. The provider rejects that combination at plan time.
<!-- schema generated by tfplugindocs -->
@@ -24,7 +27,7 @@ A volume, bind mount, or config file attached to a Dokploy service.
- `mount_path` (String) Path inside the container where the mount appears.
- `service_id` (String) ID of the service this mount attaches to. Must match `service_type` — an application ID, a compose ID, a postgres ID, and so on.
- `service_type` (String) The kind of service this mount attaches to. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`.
- `service_type` (String) The kind of service this mount attaches to. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`, `libsql`.
- `type` (String) The kind of mount to create. Valid values: `bind`, `volume`, `file`.
### Optional
+43
View File
@@ -0,0 +1,43 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_network Resource - dokploy"
subcategory: ""
description: |-
A Docker network managed by Dokploy.
Attach services to it with network_ids on dokploy_application, dokploy_compose and the database resources. Every service also joins the shared dokploy-network unless detach_dokploy_network is set.
~> Docker networks are immutable. Dokploy has no update endpoint, so changing any attribute replaces the network — which detaches the services currently using it until they redeploy.
---
# dokploy_network (Resource)
A Docker network managed by Dokploy.
Attach services to it with `network_ids` on `dokploy_application`, `dokploy_compose` and the database resources. Every service also joins the shared `dokploy-network` unless `detach_dokploy_network` is set.
~> Docker networks are immutable. Dokploy has no update endpoint, so changing any attribute replaces the network — which detaches the services currently using it until they redeploy.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) Name of the Docker network.
### Optional
- `attachable` (Boolean) Allow standalone containers to attach to an overlay network.
- `driver` (String) Network driver. Use `overlay` for multi-node Swarm clusters and `bridge` for a single host. Valid values: `bridge`, `overlay`.
- `enable_ipv4` (Boolean) Enable IPv4 address allocation.
- `enable_ipv6` (Boolean) Enable IPv6 address allocation.
- `internal` (Boolean) Isolate the network from external access.
- `ipam` (String) Custom IP address management, as a JSON object with `subnet`, `gateway` and `ipRange` keys. Leave unset to let Docker choose a subnet.
- `mtu` (Number) Maximum transmission unit for the network. Leave unset to use Docker's default.
- `server_id` (String) Remote server to create the network on. Omit to use the Dokploy host itself.
### Read-Only
- `created_at` (String) RFC 3339 timestamp of when the network was created.
- `id` (String) Unique network identifier.
- `organization_id` (String) Organization that owns the network.
+53
View File
@@ -0,0 +1,53 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_schedule Resource - dokploy"
subcategory: ""
description: |-
A cron job Dokploy runs on a schedule.
schedule_type selects where the command runs:
application — inside a running application container; set application_id.compose — inside one service of a Compose stack; set compose_id and service_name.server — on a remote server; set server_id.dokploy-server — on the Dokploy host itself.
~> A schedule targeting an application runs inside its container, so the container has to be running when the cron fires.
---
# dokploy_schedule (Resource)
A cron job Dokploy runs on a schedule.
`schedule_type` selects where the command runs:
* `application` — inside a running application container; set `application_id`.
* `compose` — inside one service of a Compose stack; set `compose_id` and `service_name`.
* `server` — on a remote server; set `server_id`.
* `dokploy-server` — on the Dokploy host itself.
~> A schedule targeting an application runs inside its container, so the container has to be running when the cron fires.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `command` (String) Command to run.
- `cron_expression` (String) Standard five-field cron expression, for example `0 3 * * *`.
- `name` (String) Display name of the schedule.
### Optional
- `app_name` (String) Docker service name the schedule targets. Derived by Dokploy when omitted.
- `application_id` (String) Application this schedule belongs to.
- `compose_id` (String) Compose stack this schedule belongs to.
- `description` (String) Free-form description.
- `enabled` (Boolean) Whether the schedule is active.
- `schedule_type` (String) Where the command runs. Valid values: `application`, `compose`, `server`, `dokploy-server`. Defaults to `application`.
- `script` (String) Multi-line script to run instead of a single command.
- `server_id` (String) Server this schedule runs on.
- `service_name` (String) Service inside a Compose stack to run the command in.
- `shell_type` (String) Shell used to interpret the command. Valid values: `bash`, `sh`. Defaults to `bash`.
- `timezone` (String) IANA timezone the cron expression is evaluated in, for example `Europe/Berlin`.
### Read-Only
- `created_at` (String) RFC 3339 timestamp of when the schedule was created.
- `id` (String) Unique schedule identifier.
+55
View File
@@ -0,0 +1,55 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_vault_provider Resource - dokploy"
subcategory: ""
description: |-
An external secret manager Dokploy resolves environment variables from at deploy time.
Reference a secret from any env value with ${{vault.<scope>.<key>}}. The value is fetched when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes effect on the next deploy with no Terraform change.
Supported providerType values: hashicorp (Vault/OpenBao), infisical, aws (Secrets Manager), doppler, azure (Key Vault) and scaleway.
config = jsonencode({
providerType = "hashicorp"
url = "https://vault.example.com"
token = var.vault_token
mount = "secret"
})
~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in config is not detected.
---
# dokploy_vault_provider (Resource)
An external secret manager Dokploy resolves environment variables from at deploy time.
Reference a secret from any `env` value with `${{vault.<scope>.<key>}}`. The value is fetched when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes effect on the next deploy with no Terraform change.
Supported `providerType` values: `hashicorp` (Vault/OpenBao), `infisical`, `aws` (Secrets Manager), `doppler`, `azure` (Key Vault) and `scaleway`.
```hcl
config = jsonencode({
providerType = "hashicorp"
url = "https://vault.example.com"
token = var.vault_token
mount = "secret"
})
```
~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in `config` is not detected.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `assignments` (String) JSON array scoping which projects or environments may resolve secrets from this provider. Pass `jsonencode([])` to leave it unscoped.
- `config` (String, Sensitive) Provider credentials as a JSON object, including the `providerType` discriminator.
- `name` (String) Name of the connection, unique within the organization.
### Read-Only
- `created_at` (String) RFC 3339 timestamp of when the connection was created.
- `id` (String) Unique vault provider identifier.
- `organization_id` (String) Organization that owns the connection.
- `provider_type` (String) Provider kind derived from `config`.
+52
View File
@@ -0,0 +1,52 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dokploy_volume_backup Resource - dokploy"
subcategory: ""
description: |-
A scheduled backup of a Docker volume to a configured dokploy_destination.
This is the counterpart to a dokploy_mount with type = "volume": the mount gives the volume a stable name, and this resource copies its contents off the host on a schedule.
Set exactly one of the *_id attributes to say which service owns the volume.
---
# dokploy_volume_backup (Resource)
A scheduled backup of a Docker volume to a configured `dokploy_destination`.
This is the counterpart to a `dokploy_mount` with `type = "volume"`: the mount gives the volume a stable name, and this resource copies its contents off the host on a schedule.
Set exactly one of the `*_id` attributes to say which service owns the volume.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `cron_expression` (String) Standard five-field cron expression, for example `0 4 * * *`.
- `destination_id` (String) Backup destination (S3-compatible bucket) to upload to.
- `name` (String) Display name of the backup job.
- `prefix` (String) Path prefix inside the destination bucket, for example `backups/shop/`.
- `volume_name` (String) Name of the Docker volume to back up.
### Optional
- `app_name` (String) Docker service name that owns the volume. Derived by Dokploy when omitted.
- `application_id` (String) Application that owns the volume.
- `compose_id` (String) Compose stack that owns the volume.
- `enabled` (Boolean) Whether the backup schedule is active.
- `keep_latest_count` (Number) Number of backups to retain. Older ones are pruned.
- `libsql_id` (String) libSQL instance that owns the volume.
- `mariadb_id` (String) MariaDB instance that owns the volume.
- `mongo_id` (String) MongoDB instance that owns the volume.
- `mysql_id` (String) MySQL instance that owns the volume.
- `postgres_id` (String) PostgreSQL instance that owns the volume.
- `redis_id` (String) Redis instance that owns the volume.
- `service_name` (String) Service inside a Compose stack that owns the volume.
- `service_type` (String) The kind of service that owns the volume. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`, `libsql`. Defaults to `application`.
- `turn_off` (Boolean) Stop the service while the backup runs. Slower, but guarantees a consistent copy of data that is being written to.
### Read-Only
- `created_at` (String) RFC 3339 timestamp of when the backup job was created.
- `id` (String) Unique volume backup identifier.
+153
View File
@@ -0,0 +1,153 @@
# Features introduced in Dokploy v0.30.0: Docker networks, vault-backed
# environment variables, scheduled jobs, and a volume that actually persists.
terraform {
required_providers {
dokploy = {
source = "maxvojtkov/dokploy"
version = "~> 0.2.0"
}
}
}
provider "dokploy" {
# host and api_key come from DOKPLOY_HOST and DOKPLOY_API_KEY
}
variable "vault_token" {
type = string
sensitive = true
}
resource "dokploy_project" "shop" {
name = "shop"
description = "Storefront and its backing services"
}
# --------------------------------------------------------------- Networking
# A private overlay network. Only the services attached to it can reach each
# other over it — the database never becomes reachable from unrelated
# services that merely share the default dokploy-network.
resource "dokploy_network" "backend" {
name = "shop-backend"
driver = "overlay"
attachable = true
internal = false
}
# ------------------------------------------------------------------ Secrets
# Environment values are resolved from Vault when the deployment runs, so
# rotating a secret takes effect on the next deploy with no Terraform change
# and no secret in Terraform state.
resource "dokploy_vault_provider" "prod" {
name = "production-vault"
config = jsonencode({
providerType = "hashicorp"
url = "https://vault.example.com"
token = var.vault_token
mount = "secret"
})
assignments = jsonencode([])
}
# ---------------------------------------------------------------- Services
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 = "set-me-from-a-variable"
network_ids = [dokploy_network.backend.id]
}
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"
network_ids = [dokploy_network.backend.id]
# Resolved from the vault provider above at deploy time.
env = <<-EOT
DATABASE_URL=postgresql://shop:$${{vault.production.db_password}}@${dokploy_postgres.db.app_name}:5432/shop
STRIPE_KEY=$${{vault.production.stripe_key}}
EOT
}
# -------------------------------------------------------- Persistent volume
# volume_name is what makes this persist. Without it Dokploy hands Docker an
# empty source and every deploy gets a fresh anonymous volume — the provider
# rejects that at plan time.
resource "dokploy_mount" "uploads" {
type = "volume"
volume_name = "shop-uploads"
mount_path = "/app/uploads"
service_type = "application"
service_id = dokploy_application.api.id
}
resource "dokploy_destination" "backups" {
name = "s3-backups"
provider_name = "s3"
access_key = "set-me"
secret_access_key = "set-me"
bucket = "shop-backups"
region = "eu-central-1"
endpoint = "https://s3.eu-central-1.amazonaws.com"
}
# The named volume is only durable if it also leaves the host.
resource "dokploy_volume_backup" "uploads" {
name = "uploads-nightly"
volume_name = dokploy_mount.uploads.volume_name
prefix = "shop/uploads/"
cron_expression = "0 4 * * *"
destination_id = dokploy_destination.backups.id
service_type = "application"
application_id = dokploy_application.api.id
keep_latest_count = 14
}
# ---------------------------------------------------------------- Schedules
resource "dokploy_schedule" "prune_sessions" {
name = "prune-sessions"
description = "Drop expired sessions every night"
schedule_type = "application"
application_id = dokploy_application.api.id
cron_expression = "0 2 * * *"
command = "node scripts/prune-sessions.js"
timezone = "Europe/Berlin"
}
# --------------------------------------------------------------------- DNS
resource "dokploy_dns_provider" "cloudflare" {
name = "cloudflare"
config = jsonencode({
providerType = "cloudflare"
apiToken = "set-me-from-a-variable"
})
}
resource "dokploy_domain" "api" {
application_id = dokploy_application.api.id
host = "api.example.com"
port = 3000
https = true
certificate_type = "letsencrypt"
# A domain can be parked without losing its configuration.
enabled = true
}
+23
View File
@@ -67,6 +67,16 @@ type ResourceSpec struct {
// discover the new ID.
ListIDs func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error)
// CreateDefaults supplies values for API fields that Dokploy's create
// schema insists on receiving but that the model cannot produce -- most
// often a name the server declines to generate. Only keys missing from
// the assembled body are added, so an explicit configuration always wins.
CreateDefaults func(model any) map[string]any
// ConfigValidators are plan-time checks that span several attributes, for
// invariants Terraform's schema cannot express on its own.
ConfigValidators []resource.ConfigValidator
// PostRead derives extra model fields from the raw read response, for
// values that are not plain top-level columns (for example a project's
// default environment, which arrives nested under `environments`).
@@ -83,6 +93,7 @@ var (
_ resource.Resource = &genericResource{}
_ resource.ResourceWithConfigure = &genericResource{}
_ resource.ResourceWithImportState = &genericResource{}
_ resource.ResourceWithConfigValidators = &genericResource{}
)
func newGenericResource(spec ResourceSpec) func() resource.Resource {
@@ -97,6 +108,10 @@ func (r *genericResource) Schema(_ context.Context, _ resource.SchemaRequest, re
resp.Schema = r.spec.Schema
}
func (r *genericResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return r.spec.ConfigValidators
}
func (r *genericResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
@@ -125,6 +140,14 @@ func (r *genericResource) Create(ctx context.Context, req resource.CreateRequest
return
}
if r.spec.CreateDefaults != nil {
for key, value := range r.spec.CreateDefaults(model) {
if _, present := body[key]; !present {
body[key] = value
}
}
}
if r.spec.NeedsOrganizationID {
orgID, err := r.api.OrganizationID(ctx)
if err != nil {
+13 -1
View File
@@ -44,7 +44,7 @@ func (p *dokployProvider) Metadata(_ context.Context, _ provider.MetadataRequest
func (p *dokployProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manage [Dokploy](https://dokploy.com) projects, environments, applications, " +
"compose stacks, databases and networking with Terraform.",
"compose stacks, databases, Docker networks and networking with Terraform.",
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Optional: true,
@@ -139,6 +139,7 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
newGenericResource(mariadbResource()),
newGenericResource(mongoResource()),
newGenericResource(redisResource()),
newGenericResource(libsqlResource()),
newGenericResource(domainResource()),
newGenericResource(mountResource()),
@@ -146,6 +147,17 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
newGenericResource(redirectResource()),
newGenericResource(securityResource()),
// Docker network management, added in Dokploy v0.30.0.
newGenericResource(networkResource()),
// Scheduling, added in Dokploy v0.30.0.
newGenericResource(scheduleResource()),
newGenericResource(volumeBackupResource()),
// External integrations, added in Dokploy v0.30.0.
newGenericResource(dnsProviderResource()),
newGenericResource(vaultProviderResource()),
newGenericResource(registryResource()),
newGenericResource(sshKeyResource()),
newGenericResource(certificateResource()),
@@ -75,6 +75,7 @@ type applicationModel struct {
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
Title types.String `tfsdk:"title" dokploy:"title,nullable"`
Subtitle types.String `tfsdk:"subtitle" dokploy:"subtitle,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled,nullable"`
@@ -113,6 +114,7 @@ type applicationModel struct {
PreviewCertType types.String `tfsdk:"preview_certificate_type" dokploy:"previewCertificateType,nullable"`
PreviewCertResolver types.String `tfsdk:"preview_custom_cert_resolver" dokploy:"previewCustomCertResolver,nullable"`
PreviewLimit types.Int64 `tfsdk:"preview_limit" dokploy:"previewLimit,nullable"`
PreviewCollabPerms types.Bool `tfsdk:"preview_require_collaborator_permissions" dokploy:"previewRequireCollaboratorPermissions,nullable"`
PreviewLabels types.List `tfsdk:"preview_labels" dokploy:"previewLabels,nullable"`
// Computed
@@ -211,6 +213,7 @@ func applicationResource() ResourceSpec {
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
"icon": optionalString("Icon shown next to the service in the Dokploy UI."),
"title": optionalString("Display title shown in the Dokploy UI."),
"subtitle": optionalString("Display subtitle shown in the Dokploy UI."),
"enabled": optionalComputedBool("Whether the application is enabled."),
@@ -247,6 +250,8 @@ func applicationResource() ResourceSpec {
"preview_certificate_type": enumString("Certificate strategy for preview deployments.", certificateTypes, false),
"preview_custom_cert_resolver": optionalString("Traefik certificate resolver for preview deployments."),
"preview_limit": optionalComputedInt("Maximum number of concurrent preview deployments."),
"preview_require_collaborator_permissions": optionalComputedBool("Only build previews for pull " +
"requests opened by users with repository collaborator permissions."),
"preview_labels": optionalComputedStringList("Pull request labels that opt into preview deployments."),
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
+13
View File
@@ -1,6 +1,7 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -19,6 +20,13 @@ type composeModel struct {
SourceType types.String `tfsdk:"source_type" dokploy:"sourceType"`
Command types.String `tfsdk:"command" dokploy:"command"`
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
CreateEnvFile types.Bool `tfsdk:"create_env_file" dokploy:"createEnvFile"`
// serviceNetworks maps a service name in the stack to the networks it
// joins, so a stack can attach per-service rather than as a whole.
ServiceNetworks jsontypes.Normalized `tfsdk:"service_networks" dokploy:"serviceNetworks,nullable"`
Repository types.String `tfsdk:"repository" dokploy:"repository,nullable"`
Owner types.String `tfsdk:"owner" dokploy:"owner,nullable"`
@@ -97,6 +105,11 @@ func composeResource() ResourceSpec {
"source_type": enumString("Where the Compose file comes from.", composeSources, false),
"command": optionalComputedString("Custom `docker compose` command to run."),
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
"icon": optionalString("Icon shown next to the stack in the Dokploy UI."),
"create_env_file": optionalComputedBool("Write the environment variables to a `.env` file next to " +
"the Compose file."),
"service_networks": optionalJSON("Per-service Docker network attachments, as a JSON object mapping " +
"each service name in the stack to an array of network IDs."),
"repository": optionalString("GitHub repository name."),
"owner": optionalString("GitHub repository owner."),
+206
View File
@@ -0,0 +1,206 @@
package provider
import (
"context"
"crypto/rand"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
)
// libSQL is the sixth managed database engine, added in Dokploy v0.30.0.
//
// Its create endpoint is the most awkward in the API: it insists on eleven
// keys being present (several only meaningfully null), it will not generate an
// appName the way the other engines do, and it returns `true` rather than the
// created row. All three are worked around below.
type libsqlModel struct {
ID types.String `tfsdk:"id" dokploy:"libsqlId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
AppName types.String `tfsdk:"app_name" dokploy:"appName"`
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
EnvironmentID types.String `tfsdk:"environment_id" dokploy:"environmentId"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
DatabaseUser types.String `tfsdk:"database_user" dokploy:"databaseUser"`
DatabasePassword types.String `tfsdk:"database_password" dokploy:"databasePassword"`
DockerImage types.String `tfsdk:"docker_image" dokploy:"dockerImage"`
SqldNode types.String `tfsdk:"sqld_node" dokploy:"sqldNode"`
SqldPrimaryURL types.String `tfsdk:"sqld_primary_url" dokploy:"sqldPrimaryUrl,nullable"`
EnableNamespaces types.Bool `tfsdk:"enable_namespaces" dokploy:"enableNamespaces"`
Command types.String `tfsdk:"command" dokploy:"command,nullable"`
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
MemoryReserve types.String `tfsdk:"memory_reservation" dokploy:"memoryReservation,nullable"`
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
Replicas types.Int64 `tfsdk:"replicas" dokploy:"replicas"`
ExternalPort types.Int64 `tfsdk:"external_port" dokploy:"externalPort,nullable"`
ExternalAdminPort types.Int64 `tfsdk:"external_admin_port" dokploy:"externalAdminPort,nullable"`
ExternalGRPCPort types.Int64 `tfsdk:"external_grpc_port" dokploy:"externalGRPCPort,nullable"`
NetworkIDs types.List `tfsdk:"network_ids" dokploy:"networkIds"`
DetachDokployNetwork types.Bool `tfsdk:"detach_dokploy_network" dokploy:"detachDokployNetwork"`
HealthCheckSwarm jsontypes.Normalized `tfsdk:"health_check_swarm" dokploy:"healthCheckSwarm,nullable"`
RestartPolicySwarm jsontypes.Normalized `tfsdk:"restart_policy_swarm" dokploy:"restartPolicySwarm,nullable"`
PlacementSwarm jsontypes.Normalized `tfsdk:"placement_swarm" dokploy:"placementSwarm,nullable"`
UpdateConfigSwarm jsontypes.Normalized `tfsdk:"update_config_swarm" dokploy:"updateConfigSwarm,nullable"`
RollbackConfigSwarm jsontypes.Normalized `tfsdk:"rollback_config_swarm" dokploy:"rollbackConfigSwarm,nullable"`
ModeSwarm jsontypes.Normalized `tfsdk:"mode_swarm" dokploy:"modeSwarm,nullable"`
LabelsSwarm jsontypes.Normalized `tfsdk:"labels_swarm" dokploy:"labelsSwarm,nullable"`
NetworkSwarm jsontypes.Normalized `tfsdk:"network_swarm" dokploy:"networkSwarm,nullable"`
EndpointSpecSwarm jsontypes.Normalized `tfsdk:"endpoint_spec_swarm" dokploy:"endpointSpecSwarm,nullable"`
StopGracePeriodSwarm types.Int64 `tfsdk:"stop_grace_period_swarm" dokploy:"stopGracePeriodSwarm,nullable"`
ApplicationStatus types.String `tfsdk:"application_status" dokploy:"applicationStatus,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func libsqlResource() ResourceSpec {
return ResourceSpec{
Name: "libsql",
UpdateAfterCreate: true,
CreateProc: "libsql.create",
ReadProc: "libsql.one",
UpdateProc: "libsql.update",
DeleteProc: "libsql.remove",
NewModel: func() any { return &libsqlModel{} },
// `libsql.create` rejects a body that merely omits a key it considers
// required, even when null is the only sensible value, and it refuses
// to generate an appName. Fill both in.
CreateDefaults: func(model any) map[string]any {
libsql, ok := model.(*libsqlModel)
if !ok {
return nil
}
defaults := map[string]any{
"description": nil,
"serverId": nil,
"sqldPrimaryUrl": nil,
}
if libsql.AppName.IsNull() || libsql.AppName.IsUnknown() {
defaults["appName"] = generateAppName(libsql.Name.ValueString())
}
return defaults
},
// `libsql.create` returns `true`, so the new ID is found by diffing the
// environment's libSQL list.
ListIDs: func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error) {
libsql, ok := model.(*libsqlModel)
if !ok {
return nil, fmt.Errorf("expected *libsqlModel, got %T", model)
}
raw, err := api.Query(ctx, "environment.one", map[string]any{
"environmentId": libsql.EnvironmentID.ValueString(),
})
if err != nil {
return nil, err
}
return collectNestedIDs(raw, "libsql", "libsqlId")
},
Schema: schema.Schema{
MarkdownDescription: databaseNote("libSQL") + "\n\n" +
"libSQL runs as a `sqld` server. A `primary` node owns the data; a `replica` node follows a " +
"primary named by `sqld_primary_url`.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique libSQL identifier."),
"name": requiredString("Display name of the database."),
"app_name": optionalComputedReplaceString("Unique Docker service name. Generated from `name` when " +
"omitted, because Dokploy's libSQL endpoint does not generate one. Changing it forces a new database."),
"description": optionalString("Free-form description."),
"environment_id": requiredReplaceString("Environment this database belongs to."),
"server_id": optionalReplaceString("Remote server to deploy on. Omit to use the Dokploy host itself."),
"database_user": requiredString("Database user to create."),
"database_password": sensitiveString("Password for the database user.", true),
"docker_image": requiredString("libSQL server image to run, for example " +
"`ghcr.io/tursodatabase/libsql-server:latest`."),
"sqld_node": enumStringWithDefault("Role this node plays in a libSQL cluster.", sqldNodes, "primary"),
"sqld_primary_url": optionalString("URL of the primary node, when `sqld_node` is `replica`."),
"enable_namespaces": optionalComputedBool("Serve multiple logical databases from one instance " +
"through libSQL namespaces."),
"command": optionalString("Override the container entrypoint command."),
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
"memory_reservation": optionalString("Soft memory reservation, for example `256m`."),
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
"replicas": optionalComputedInt("Number of replicas to run."),
"external_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the HTTP API."},
"external_admin_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the admin API."},
"external_grpc_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the gRPC replication endpoint."},
"network_ids": optionalComputedStringList("IDs of additional Docker networks to attach."),
"detach_dokploy_network": optionalComputedBool("Detach the service from the shared `dokploy-network`."),
"health_check_swarm": optionalJSON("Docker Swarm health check configuration, as a JSON object."),
"restart_policy_swarm": optionalJSON("Docker Swarm restart policy, as a JSON object."),
"placement_swarm": optionalJSON("Docker Swarm placement constraints, as a JSON object."),
"update_config_swarm": optionalJSON("Docker Swarm rolling update configuration, as a JSON object."),
"rollback_config_swarm": optionalJSON("Docker Swarm rollback configuration, as a JSON object."),
"mode_swarm": optionalJSON("Docker Swarm service mode, as a JSON object."),
"labels_swarm": optionalJSON("Docker Swarm service labels, as a JSON object."),
"network_swarm": optionalJSON("Docker Swarm network attachments, as a JSON array."),
"endpoint_spec_swarm": optionalJSON("Docker Swarm endpoint specification, as a JSON object."),
"stop_grace_period_swarm": schema.Int64Attribute{Optional: true, MarkdownDescription: "Grace period in nanoseconds before a container is killed."},
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
"created_at": computedString("RFC 3339 timestamp of when the database was created."),
},
},
}
}
// generateAppName mirrors how Dokploy names a service: a slug of the display
// name plus a short random suffix, so two databases called "cache" in
// different projects do not collide on the Docker host.
func generateAppName(name string) string {
var slug strings.Builder
lastDash := true
for _, r := range strings.ToLower(name) {
switch {
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
slug.WriteRune(r)
lastDash = false
case !lastDash:
slug.WriteByte('-')
lastDash = true
}
}
base := strings.Trim(slug.String(), "-")
if base == "" {
base = "libsql"
}
return base + "-" + randomSuffix(6)
}
const suffixAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
func randomSuffix(n int) string {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
// crypto/rand does not fail in practice; a fixed suffix still yields a
// usable name and Dokploy rejects a genuine collision.
return strings.Repeat("0", n)
}
for i, b := range buf {
buf[i] = suffixAlphabet[int(b)%len(suffixAlphabet)]
}
return string(buf)
}
+94
View File
@@ -0,0 +1,94 @@
package provider_test
import (
"regexp"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
const testMountProviderBlock = `
provider "dokploy" {
host = "https://dokploy.invalid"
api_key = "not-used-plan-only"
}
`
// A `volume` mount without a volume_name is the dangerous case: Dokploy's
// generateVolumeMounts maps a null volumeName to `Source: ""`, which Docker
// reads as an anonymous volume. Every deploy then gets a brand-new volume and
// the previous one is orphaned, so the data silently does not survive.
//
// These validations run at plan time, so they need no Dokploy instance.
func TestMountValidation(t *testing.T) {
for name, tc := range map[string]struct {
config string
expectError *regexp.Regexp
}{
"volume without volume_name": {
config: testMountProviderBlock + `
resource "dokploy_mount" "test" {
type = "volume"
mount_path = "/data"
service_type = "application"
service_id = "app-123"
}`,
expectError: regexp.MustCompile(`volume_name`),
},
"volume with empty volume_name": {
config: testMountProviderBlock + `
resource "dokploy_mount" "test" {
type = "volume"
mount_path = "/data"
volume_name = ""
service_type = "application"
service_id = "app-123"
}`,
expectError: regexp.MustCompile(`volume_name`),
},
"bind without host_path": {
config: testMountProviderBlock + `
resource "dokploy_mount" "test" {
type = "bind"
mount_path = "/data"
service_type = "application"
service_id = "app-123"
}`,
expectError: regexp.MustCompile(`host_path`),
},
"file without file_path": {
config: testMountProviderBlock + `
resource "dokploy_mount" "test" {
type = "file"
mount_path = "/etc/app"
content = "hello"
service_type = "application"
service_id = "app-123"
}`,
expectError: regexp.MustCompile(`file_path`),
},
"volume_name set on a bind mount": {
config: testMountProviderBlock + `
resource "dokploy_mount" "test" {
type = "bind"
mount_path = "/data"
host_path = "/srv/data"
volume_name = "stray"
service_type = "application"
service_id = "app-123"
}`,
expectError: regexp.MustCompile(`volume_name`),
},
} {
t.Run(name, func(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{{
Config: tc.config,
PlanOnly: true,
ExpectError: tc.expectError,
}},
})
})
}
}
+96
View File
@@ -0,0 +1,96 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Docker networks became first-class in Dokploy v0.30.0. Services attach to
// them through `network_ids` on the application, compose and database
// resources.
type networkModel struct {
ID types.String `tfsdk:"id" dokploy:"networkId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Driver types.String `tfsdk:"driver" dokploy:"driver"`
Internal types.Bool `tfsdk:"internal" dokploy:"internal"`
Attachable types.Bool `tfsdk:"attachable" dokploy:"attachable"`
EnableIPv4 types.Bool `tfsdk:"enable_ipv4" dokploy:"enableIPv4"`
EnableIPv6 types.Bool `tfsdk:"enable_ipv6" dokploy:"enableIPv6"`
MTU types.Int64 `tfsdk:"mtu" dokploy:"mtu,nullable"`
// IPAM is Docker's address-management block: {"subnet","gateway","ipRange"}.
IPAM jsontypes.Normalized `tfsdk:"ipam" dokploy:"ipam,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func networkResource() ResourceSpec {
// Dokploy exposes no `network.update`: a Docker network's driver, subnet
// and flags are fixed once it exists. Every configurable attribute
// therefore forces replacement.
return ResourceSpec{
Name: "network",
CreateProc: "network.create",
ReadProc: "network.one",
DeleteProc: "network.remove",
NewModel: func() any { return &networkModel{} },
Schema: schema.Schema{
MarkdownDescription: "A Docker network managed by Dokploy.\n\n" +
"Attach services to it with `network_ids` on `dokploy_application`, `dokploy_compose` and the " +
"database resources. Every service also joins the shared `dokploy-network` unless " +
"`detach_dokploy_network` is set.\n\n" +
"~> Docker networks are immutable. Dokploy has no update endpoint, so changing any attribute " +
"replaces the network — which detaches the services currently using it until they redeploy.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique network identifier."),
"name": requiredReplaceString("Name of the Docker network."),
"driver": schema.StringAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Network driver. Use `overlay` for multi-node Swarm clusters and `bridge` for a single host. Valid values: `bridge`, `overlay`.",
Validators: enumValidator(networkDrivers),
PlanModifiers: requiresReplaceString(),
},
"internal": replaceBool("Isolate the network from external access.", false),
"attachable": replaceBool("Allow standalone containers to attach to an overlay network.", false),
"enable_ipv4": replaceBool("Enable IPv4 address allocation.", true),
"enable_ipv6": replaceBool("Enable IPv6 address allocation.", false),
"mtu": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum transmission unit for the network. Leave unset to use Docker's default.",
PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()},
},
"ipam": schema.StringAttribute{
Optional: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Custom IP address management, as a JSON object with `subnet`, `gateway` " +
"and `ipRange` keys. Leave unset to let Docker choose a subnet.",
PlanModifiers: requiresReplaceString(),
},
"server_id": optionalReplaceString("Remote server to create the network on. Omit to use the Dokploy host itself."),
"organization_id": computedString("Organization that owns the network."),
"created_at": computedString("RFC 3339 timestamp of when the network was created."),
},
},
}
}
// replaceBool is an optional boolean with a fixed default that cannot be
// changed in place.
func replaceBool(description string, def bool) schema.BoolAttribute {
return schema.BoolAttribute{
Optional: true,
Computed: true,
MarkdownDescription: description,
Default: booldefault.StaticBool(def),
PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()},
}
}
+51 -1
View File
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -27,6 +29,7 @@ type domainModel struct {
StripPath types.Bool `tfsdk:"strip_path" dokploy:"stripPath"`
Middlewares types.List `tfsdk:"middlewares" dokploy:"middlewares"`
ForwardAuthEnabled types.Bool `tfsdk:"forward_auth_enabled" dokploy:"forwardAuthEnabled"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled,nullable"`
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,create"`
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,create"`
PreviewDeploymentID types.String `tfsdk:"preview_deployment_id" dokploy:"previewDeploymentId,create"`
@@ -66,6 +69,9 @@ func domainResource() ResourceSpec {
"strip_path": optionalComputedBool("Strip `path` from the request before forwarding it."),
"middlewares": optionalComputedStringList("Names of Traefik middlewares to apply."),
"forward_auth_enabled": optionalComputedBool("Protect this domain with Dokploy's forward auth."),
"enabled": optionalComputedBool("Whether the domain is served. Setting this to `false` removes " +
"the route from Traefik but keeps the certificate, path and middleware configuration intact, " +
"so the domain can be brought back without reconfiguring it."),
"application_id": optionalReplaceString("Application this domain routes to."),
"compose_id": optionalReplaceString("Compose stack this domain routes to."),
"preview_deployment_id": optionalReplaceString("Preview deployment this domain routes to."),
@@ -92,6 +98,43 @@ type mountModel struct {
ServiceID types.String `tfsdk:"service_id" dokploy:"serviceId,create"`
}
// mountConfigValidators enforce the type/field pairing that Dokploy itself
// does not.
//
// Dokploy's `generateVolumeMounts` renders a mount as
// `{Source: mount.volumeName || "", Target: mount.mountPath}`. A `volume`
// mount whose volumeName is null therefore reaches Docker with an empty
// source, which Docker treats as an *anonymous* volume: a fresh one is created
// on every deploy and the previous one is left orphaned, so the data silently
// never survives a redeploy. `mounts.create` accepts the mount regardless, so
// nothing surfaces until the data is already gone.
//
// The same shape applies to `bind` (hostPath) and `file` (filePath).
func mountConfigValidators() []resource.ConfigValidator {
return []resource.ConfigValidator{
&requiredWhen{
discriminator: path.Root("type"),
value: "volume",
attribute: path.Root("volume_name"),
rationale: "Dokploy passes an unset `volume_name` to Docker as an empty source, which creates " +
"a new anonymous volume on every deploy. The data written to the previous volume is " +
"orphaned and never reused, so the mount silently does not persist anything.",
},
&requiredWhen{
discriminator: path.Root("type"),
value: "bind",
attribute: path.Root("host_path"),
rationale: "A bind mount with no host path has nothing to bind to.",
},
&requiredWhen{
discriminator: path.Root("type"),
value: "file",
attribute: path.Root("file_path"),
rationale: "Dokploy writes `content` to `file_path` inside the service's files directory.",
},
}
}
func mountResource() ResourceSpec {
return ResourceSpec{
Name: "mount",
@@ -100,11 +143,18 @@ func mountResource() ResourceSpec {
UpdateProc: "mounts.update",
DeleteProc: "mounts.remove",
NewModel: func() any { return &mountModel{} },
ConfigValidators: mountConfigValidators(),
Schema: schema.Schema{
MarkdownDescription: "A volume, bind mount, or config file attached to a Dokploy service.\n\n" +
"* `type = \"volume\"` — a named Docker volume; set `volume_name`.\n" +
"* `type = \"bind\"` — a path on the host; set `host_path`.\n" +
"* `type = \"file\"` — a file rendered from `content`; set `file_path`.",
"* `type = \"file\"` — a file rendered from `content`; set `file_path`.\n\n" +
"~> **A `volume` mount must set `volume_name`.** Dokploy hands an unset name to Docker as an " +
"empty source, which creates a fresh anonymous volume on every deploy and orphans the " +
"previous one — the data never survives a redeploy. The provider rejects that combination " +
"at plan time.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique mount identifier."),
"type": enumString("The kind of mount to create.", mountTypes, true),
+131
View File
@@ -0,0 +1,131 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// DNS providers and vault providers both arrived in Dokploy v0.30.0. They
// share a shape: a name plus a free-form `config` object whose keys depend on
// a `providerType` discriminator nested inside it.
//
// Dokploy masks the credentials in `config` on every read, so the value is
// tagged `noread`: the configured value stays authoritative in state instead
// of being overwritten with asterisks on the next refresh.
// ---------------------------------------------------------- DNS provider
type dnsProviderModel struct {
ID types.String `tfsdk:"id" dokploy:"dnsProviderId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func dnsProviderResource() ResourceSpec {
return ResourceSpec{
Name: "dns_provider",
CreateProc: "dnsProvider.create",
ReadProc: "dnsProvider.one",
UpdateProc: "dnsProvider.update",
DeleteProc: "dnsProvider.remove",
NewModel: func() any { return &dnsProviderModel{} },
Schema: schema.Schema{
MarkdownDescription: "A DNS provider connection Dokploy uses to create records for domains " +
"automatically.\n\n" +
"`config` is a JSON object whose shape depends on `providerType`:\n\n" +
"```hcl\n" +
"# Cloudflare\n" +
"config = jsonencode({ providerType = \"cloudflare\", apiToken = var.cloudflare_token })\n\n" +
"# AWS Route53\n" +
"config = jsonencode({\n" +
" providerType = \"route53\"\n" +
" accessKeyId = var.aws_access_key_id\n" +
" secretAccessKey = var.aws_secret_access_key\n" +
"})\n" +
"```\n\n" +
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
"you configured. Drift in `config` is not detected.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique DNS provider identifier."),
"name": requiredString("Name of the connection. Must be unique within the organization and may " +
"contain only letters, digits, `-` and `_`."),
"config": schema.StringAttribute{
Required: true,
Sensitive: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
},
"provider_type": computedString("Provider kind derived from `config`: `cloudflare` or `route53`."),
"organization_id": computedString("Organization that owns the connection."),
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
},
},
}
}
// -------------------------------------------------------- Vault provider
type vaultProviderModel struct {
ID types.String `tfsdk:"id" dokploy:"vaultProviderId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
Assignments jsontypes.Normalized `tfsdk:"assignments" dokploy:"assignments"`
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func vaultProviderResource() ResourceSpec {
return ResourceSpec{
Name: "vault_provider",
CreateProc: "vaultProvider.create",
ReadProc: "vaultProvider.one",
UpdateProc: "vaultProvider.update",
DeleteProc: "vaultProvider.remove",
NewModel: func() any { return &vaultProviderModel{} },
Schema: schema.Schema{
MarkdownDescription: "An external secret manager Dokploy resolves environment variables from at " +
"deploy time.\n\n" +
"Reference a secret from any `env` value with `${{vault.<scope>.<key>}}`. The value is fetched " +
"when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes " +
"effect on the next deploy with no Terraform change.\n\n" +
"Supported `providerType` values: `hashicorp` (Vault/OpenBao), `infisical`, `aws` " +
"(Secrets Manager), `doppler`, `azure` (Key Vault) and `scaleway`.\n\n" +
"```hcl\n" +
"config = jsonencode({\n" +
" providerType = \"hashicorp\"\n" +
" url = \"https://vault.example.com\"\n" +
" token = var.vault_token\n" +
" mount = \"secret\"\n" +
"})\n" +
"```\n\n" +
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
"you configured. Drift in `config` is not detected.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique vault provider identifier."),
"name": requiredString("Name of the connection, unique within the organization."),
"config": schema.StringAttribute{
Required: true,
Sensitive: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
},
"assignments": schema.StringAttribute{
Required: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "JSON array scoping which projects or environments may resolve secrets " +
"from this provider. Pass `jsonencode([])` to leave it unscoped.",
},
"provider_type": computedString("Provider kind derived from `config`."),
"organization_id": computedString("Organization that owns the connection."),
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
},
},
}
}
+146
View File
@@ -0,0 +1,146 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ------------------------------------------------------------- Schedule
type scheduleModel struct {
ID types.String `tfsdk:"id" dokploy:"scheduleId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
Command types.String `tfsdk:"command" dokploy:"command"`
Script types.String `tfsdk:"script" dokploy:"script,nullable"`
ShellType types.String `tfsdk:"shell_type" dokploy:"shellType"`
ScheduleType types.String `tfsdk:"schedule_type" dokploy:"scheduleType"`
Timezone types.String `tfsdk:"timezone" dokploy:"timezone,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func scheduleResource() ResourceSpec {
return ResourceSpec{
Name: "schedule",
CreateProc: "schedule.create",
ReadProc: "schedule.one",
UpdateProc: "schedule.update",
DeleteProc: "schedule.delete",
NewModel: func() any { return &scheduleModel{} },
Schema: schema.Schema{
MarkdownDescription: "A cron job Dokploy runs on a schedule.\n\n" +
"`schedule_type` selects where the command runs:\n\n" +
"* `application` — inside a running application container; set `application_id`.\n" +
"* `compose` — inside one service of a Compose stack; set `compose_id` and `service_name`.\n" +
"* `server` — on a remote server; set `server_id`.\n" +
"* `dokploy-server` — on the Dokploy host itself.\n\n" +
"~> A schedule targeting an application runs inside its container, so the container has to be " +
"running when the cron fires.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique schedule identifier."),
"name": requiredString("Display name of the schedule."),
"description": optionalString("Free-form description."),
"cron_expression": requiredString("Standard five-field cron expression, for example `0 3 * * *`."),
"command": requiredString("Command to run."),
"script": optionalString("Multi-line script to run instead of a single command."),
"shell_type": enumStringWithDefault("Shell used to interpret the command.", shellTypes, "bash"),
"schedule_type": enumStringWithDefault("Where the command runs.", scheduleTypes, "application"),
"timezone": optionalString("IANA timezone the cron expression is evaluated in, for example `Europe/Berlin`."),
"enabled": optionalComputedBool("Whether the schedule is active."),
"app_name": optionalComputedString("Docker service name the schedule targets. Derived by Dokploy when omitted."),
"service_name": optionalString("Service inside a Compose stack to run the command in."),
"application_id": optionalReplaceString("Application this schedule belongs to."),
"compose_id": optionalReplaceString("Compose stack this schedule belongs to."),
"server_id": optionalReplaceString("Server this schedule runs on."),
"created_at": computedString("RFC 3339 timestamp of when the schedule was created."),
},
},
}
}
// -------------------------------------------------------- Volume backup
type volumeBackupModel struct {
ID types.String `tfsdk:"id" dokploy:"volumeBackupId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
VolumeName types.String `tfsdk:"volume_name" dokploy:"volumeName"`
Prefix types.String `tfsdk:"prefix" dokploy:"prefix"`
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
DestinationID types.String `tfsdk:"destination_id" dokploy:"destinationId"`
ServiceType types.String `tfsdk:"service_type" dokploy:"serviceType"`
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
TurnOff types.Bool `tfsdk:"turn_off" dokploy:"turnOff"`
KeepLatestCount types.Int64 `tfsdk:"keep_latest_count" dokploy:"keepLatestCount,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
PostgresID types.String `tfsdk:"postgres_id" dokploy:"postgresId,nullable"`
MySQLID types.String `tfsdk:"mysql_id" dokploy:"mysqlId,nullable"`
MariaDBID types.String `tfsdk:"mariadb_id" dokploy:"mariadbId,nullable"`
MongoID types.String `tfsdk:"mongo_id" dokploy:"mongoId,nullable"`
RedisID types.String `tfsdk:"redis_id" dokploy:"redisId,nullable"`
LibsqlID types.String `tfsdk:"libsql_id" dokploy:"libsqlId,nullable"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func volumeBackupResource() ResourceSpec {
return ResourceSpec{
Name: "volume_backup",
CreateProc: "volumeBackups.create",
ReadProc: "volumeBackups.one",
UpdateProc: "volumeBackups.update",
DeleteProc: "volumeBackups.delete",
NewModel: func() any { return &volumeBackupModel{} },
Schema: schema.Schema{
MarkdownDescription: "A scheduled backup of a Docker volume to a configured " +
"`dokploy_destination`.\n\n" +
"This is the counterpart to a `dokploy_mount` with `type = \"volume\"`: the mount gives the " +
"volume a stable name, and this resource copies its contents off the host on a schedule.\n\n" +
"Set exactly one of the `*_id` attributes to say which service owns the volume.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique volume backup identifier."),
"name": requiredString("Display name of the backup job."),
"volume_name": requiredString("Name of the Docker volume to back up."),
"prefix": requiredString("Path prefix inside the destination bucket, for example `backups/shop/`."),
"cron_expression": requiredString("Standard five-field cron expression, for example `0 4 * * *`."),
"destination_id": requiredString("Backup destination (S3-compatible bucket) to upload to."),
"service_type": enumStringWithDefault("The kind of service that owns the volume.", volumeBackupServiceTypes, "application"),
"app_name": optionalComputedString("Docker service name that owns the volume. Derived by Dokploy when omitted."),
"service_name": optionalString("Service inside a Compose stack that owns the volume."),
"turn_off": optionalComputedBool("Stop the service while the backup runs. Slower, but guarantees a " +
"consistent copy of data that is being written to."),
"keep_latest_count": optionalComputedInt("Number of backups to retain. Older ones are pruned."),
"enabled": optionalComputedBool("Whether the backup schedule is active."),
"application_id": optionalReplaceString("Application that owns the volume."),
"compose_id": optionalReplaceString("Compose stack that owns the volume."),
"postgres_id": optionalReplaceString("PostgreSQL instance that owns the volume."),
"mysql_id": optionalReplaceString("MySQL instance that owns the volume."),
"mariadb_id": optionalReplaceString("MariaDB instance that owns the volume."),
"mongo_id": optionalReplaceString("MongoDB instance that owns the volume."),
"redis_id": optionalReplaceString("Redis instance that owns the volume."),
"libsql_id": optionalReplaceString("libSQL instance that owns the volume."),
"created_at": computedString("RFC 3339 timestamp of when the backup job was created."),
},
},
}
}
+18 -1
View File
@@ -196,6 +196,12 @@ func optionalJSON(description string) schema.StringAttribute {
}
}
// enumValidator is the validator list for a string constrained to a fixed set,
// for attributes assembled by hand rather than through enumString.
func enumValidator(values []string) []validator.String {
return []validator.String{stringvalidator.OneOf(values...)}
}
func joinBackticked(values []string) string {
out := ""
for i, v := range values {
@@ -217,7 +223,18 @@ var (
composeSources = []string{"git", "github", "gitlab", "bitbucket", "gitea", "raw"}
domainTypes = []string{"compose", "application", "preview"}
mountTypes = []string{"bind", "volume", "file"}
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose"}
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql"}
protocolTypes = []string{"tcp", "udp"}
publishModes = []string{"ingress", "host"}
// Added in Dokploy v0.30.0.
networkDrivers = []string{"bridge", "overlay"}
shellTypes = []string{"bash", "sh"}
scheduleTypes = []string{"application", "compose", "server", "dokploy-server"}
sqldNodes = []string{"primary", "replica"}
// volumeBackups accepts the service types plus libsql.
volumeBackupServiceTypes = []string{
"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql",
}
)
+78
View File
@@ -0,0 +1,78 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// requiredWhen declares that `attribute` must hold a non-empty value whenever
// `discriminator` equals `value`, and must be absent otherwise.
//
// Terraform's schema language cannot express "required, but only for this
// variant", and Dokploy's Zod schemas accept every combination -- so without a
// provider-side check a nonsensical resource is created without complaint. The
// `dokploy_mount` case is the reason this exists: see mountConfigValidators.
type requiredWhen struct {
discriminator path.Path
value string
attribute path.Path
// rationale explains the consequence of getting it wrong, so the error
// tells the practitioner why rather than only what.
rationale string
}
var _ resource.ConfigValidator = &requiredWhen{}
func (v *requiredWhen) Description(ctx context.Context) string {
return v.MarkdownDescription(ctx)
}
func (v *requiredWhen) MarkdownDescription(_ context.Context) string {
return fmt.Sprintf("`%s` is required when `%s` is `%s`, and must not be set otherwise.",
v.attribute, v.discriminator, v.value)
}
func (v *requiredWhen) ValidateResource(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var discriminator types.String
resp.Diagnostics.Append(req.Config.GetAttribute(ctx, v.discriminator, &discriminator)...)
if resp.Diagnostics.HasError() || discriminator.IsNull() || discriminator.IsUnknown() {
return
}
var attribute types.String
resp.Diagnostics.Append(req.Config.GetAttribute(ctx, v.attribute, &attribute)...)
if resp.Diagnostics.HasError() || attribute.IsUnknown() {
// An unknown value cannot be checked at plan time; it is resolved
// during apply and Dokploy validates it there.
return
}
matches := discriminator.ValueString() == v.value
empty := attribute.IsNull() || attribute.ValueString() == ""
switch {
case matches && empty:
detail := fmt.Sprintf("`%s` must be set to a non-empty value when `%s` is `%s`.",
v.attribute, v.discriminator, v.value)
if v.rationale != "" {
detail += "\n\n" + v.rationale
}
resp.Diagnostics.AddAttributeError(v.attribute,
fmt.Sprintf("Missing %s", v.attribute), detail)
case !matches && !empty:
resp.Diagnostics.AddAttributeError(v.attribute,
fmt.Sprintf("Unexpected %s", v.attribute),
fmt.Sprintf("`%s` only applies when `%s` is `%s`, but it is `%s`. "+
"Dokploy ignores the value, so leaving it set hides a mistake.",
v.attribute, v.discriminator, v.value, discriminator.ValueString()))
}
}