wihan/dev

docs /dokploy / guides

markdown
browse the dokploy docs

Adopting an existing Dokploy instance

Most people reach this provider with a Dokploy server already running. This guide brings that server under Terraform management by importing it, never by recreating it.

Decide what Terraform owns

Before importing anything, decide per service whether Terraform or the Dokploy UI owns it. This is not a stylistic choice.

dokploy_application and dokploy_compose own the whole service. Applying either rewrites the service’s source, build and environment configuration wholesale, so anything changed in the Dokploy UI is replaced on the next apply. Manage a given application or compose service in Terraform or in the UI, not both.

As of v0.4.0 dokploy_application no longer writes any field it does not model: watch_paths, build_secrets, create_env_file, enable_submodules, is_static_spa, trigger_type, heroku_version and railpack_version are all schema attributes, and a pair of reflection tests (TestDialectARequestsCarryNoBlindFields, TestSaveRequestsReadEveryFieldFromTheModel) fail the build if a future field is added to one of these endpoints without one. So the rewrite is total but not lossy: what the resource writes is what the schema models, and nothing is silently blanked.

Database engines own their data mount

A dokploy_postgres (or dokploy_mysql, dokploy_mariadb, dokploy_mongo, dokploy_redis) creates a volume mount for its data directory the moment it is created, and owns it from then on. That mount belongs to the engine resource, and Terraform must not be given a second claim on it:

  • Do not import it as a dokploy_mount. generate_imports.py skips it for you, but a hand-written import block would not.
  • Do not declare one in fresh configuration either. The same rule applies to a stack you are writing from scratch, not just to one you are adopting: a dokploy_mount pointing at a database’s data directory hands Terraform a volume the engine resource already recreates on its own, and terraform destroy on that mount would delete the database’s data directory.

dokploy_mount on a database engine is for additional volumes - config files, seed scripts, an extra data directory you manage yourself - never for the engine’s own data directory.

Enumerate what is on the server

The repository ships a read-only harness under dogfood/. introspect.py and generate_imports.py issue HTTP GET only, and Dokploy exposes every mutation as POST; dry-run.sh issues no HTTP itself - it drives terraform and shells out to those same two scripts - so none of the three can modify the server.

export DOKPLOY_ENDPOINT=https://dokploy.example.com
export DOKPLOY_API_KEY=...

./dogfood/introspect.py

introspect.py enumerates projects, environments, services, domains and their child resources. Secrets are reported as a length, never printed.

generate_imports.py then emits Terraform import blocks for every live resource this provider supports, with one deliberate exception: each database engine’s auto-created data-volume mount (type == "volume" and volumeName == appName + "-data") is skipped, marked with a # skipped <id>: ... comment in imports.tf rather than silently omitted. Importing it as a dokploy_mount would hand Terraform a volume the engine resource already recreates on its own, and terraform destroy on that mount would delete the database’s data directory.

The -generate-config-out gap, and why the harness patches around it

terraform plan -generate-config-out cannot, by itself, produce a config that later plans cleanly for any stack containing a dokploy_postgres, dokploy_mysql, dokploy_mariadb, dokploy_mongo or dokploy_redis resource. All five engines are affected. You get:

Error: Missing Configuration for Required Attribute
  with dokploy_mysql.<label>, on generated.tf line N:
  Must set a configuration value for the database_password attribute as
  the provider has marked it as required.

database_password is Required and Sensitive in every engine’s schema, correctly so: the server genuinely requires a caller-supplied password and never generates one. Terraform’s config generation refuses to write a value for any Sensitive attribute, emitting null # sensitive instead, and a null on a Required attribute is rejected by Terraform Core before any provider code runs. That error is Terraform Core’s own wording, not this provider’s.

This is a structural gap in -generate-config-out, not a provider read-path bug: hand-patching the real password into generated.tf and continuing converges to No changes cleanly.

dogfood/dry-run.sh handles this for you. It tolerates that one command’s nonzero exit, verifies generated.tf was still written, then runs generate_imports.py --patch-sensitive, which finds each <attr> = null # sensitive marker by pattern and fills in the real value from the same read-only .one endpoint the harness already calls.

DOKPLOY_DOGFOOD=1 ./dogfood/dry-run.sh

dry-run.sh never runs terraform apply. It imports into a throwaway state, generates config, and requires a second plan to be empty. On success it deletes its scratch directory; on any failure it leaves dogfood/scratch/ in place for inspection.

Two things import cannot do

Provider-only attributes do not survive import. deploy_on_change and deployment_timeout exist only in Terraform, so import seeds them with their schema defaults (true and "15m"). Importing a resource whose configuration sets a non-default value plans one diff to reconcile it. That diff is expected and settles on the first apply.

The default production environment cannot be deleted through Dokploy’s API, so terraform destroy on an imported one fails by design. Remove it from state instead:

terraform state rm dokploy_environment.production

After adoption

Run a plan and confirm it is empty. A zero-diff plan that stays zero-diff across subsequent applies is the signal that adoption succeeded.

For what triggers a redeploy once Terraform owns a service, see Deploy semantics.