skip to content
wihan.dev
view as markdown
browse the dokploy docs

Secrets and sensitive values

env is not sensitive, by design

Dokploy stores the environment of a service as one multiline KEY=value string. The provider exposes it as a plain string:

hcl
resource "dokploy_application" "web" {
  name           = "web"
  environment_id = dokploy_project.example.production_environment_id

  docker = {
    image = "traefik/whoami:v1.10"
  }

  env = <<-EOT
    LOG_LEVEL=info
    PORT=80
  EOT
}

The provider deliberately does not mark env as sensitive. Most of the content of env is ordinary configuration. A sensitive env would redact the whole string from each plan, and the diffs would become unreadable for the common case.

Mark the values that are secret at the point where they enter the configuration:

hcl
variable "api_token" {
  type      = string
  sensitive = true
}

resource "dokploy_application" "web" {
  name           = "web"
  environment_id = dokploy_project.example.production_environment_id

  docker = {
    image = "traefik/whoami:v1.10"
  }

  env = <<-EOT
    LOG_LEVEL=info
    API_TOKEN=${var.api_token}
  EOT
}

Terraform propagates sensitivity through interpolation. When you interpolate a sensitive variable into env, the plan output redacts the whole env value.

One behavior to know: dokploy_application writes env and build_secrets in the same request. A configuration that sets env but omits build_secrets clears the build secrets in Dokploy. If you need build secrets, set build_secrets explicitly.

Database passwords

database_password is Sensitive on all six engines. See dokploy_postgres for the attribute in context. dokploy_mysql, dokploy_mariadb, dokploy_mongo, dokploy_redis, and dokploy_libsql declare it in the same way. The server requires a password from the caller and never generates one. Set database_password, or set its write-only companion database_password_wo (see Write-only companions below).

Two consequences:

  • A password change takes effect on the next deploy, not on write. See Deploy semantics.
  • terraform plan -generate-config-out cannot produce a working configuration for a stack with a database engine. Terraform refuses to write a value for a sensitive attribute, and it then rejects the null on a required attribute. See Adopt an existing Dokploy server for the workaround.

The database_root_password of MySQL and MariaDB is also sensitive. It is Optional and Computed, so it does not cause the same generation failure. It has a write-only companion too, database_root_password_wo.

Write-only companions

Terraform 1.11 added write-only arguments: values that a provider sends to the server and that Terraform keeps out of the plan and the state. Every secret attribute of this provider has one, as a pair next to the plain attribute:

  • <name>_wo, the write-only form, sensitive.
  • <name>_wo_version, a number that gates a new value.

The pairs:

Resource Plain attribute Companion
The six database engines database_password database_password_wo
dokploy_mysql, dokploy_mariadb database_root_password database_root_password_wo
dokploy_destination access_key, secret_access_key access_key_wo, secret_access_key_wo
dokploy_security password password_wo
dokploy_vault_provider the secret of each config block, for example hashicorp.token hashicorp.token_wo, and so on
dokploy_ssh_key private_key private_key_wo (a new version replaces the key)
dokploy_certificate private_key private_key_wo
dokploy_registry password password_wo
dokploy_ai api_key api_key_wo
dokploy_gitlab_provider secret secret_wo
dokploy_bitbucket_provider app_password, api_token app_password_wo, api_token_wo
dokploy_gitea_provider client_secret client_secret_wo
The notification resources the channel secret: webhook_url, bot_token, password, api_key, app_token, access_token, user_key the same name with _wo
dokploy_user password password_wo (a new version replaces the account)

Set the plain attribute or its companion, not both. A validator rejects a configuration that sets both, and one that sets neither where the server needs a value.

hcl
variable "db_password" {
  type      = string
  sensitive = true
}

resource "dokploy_postgres" "db" {
  name                         = "db"
  environment_id               = dokploy_project.example.production_environment_id
  database_name                = "app"
  database_user                = "app"
  database_password_wo         = var.db_password
  database_password_wo_version = 1
}

The rules:

  • The value reaches the server on create, and when <name>_wo_version changes. A new value with the same version does not reach the server, and the plan is empty. To rotate a secret, change the value and increase the version.
  • The state holds null for the plain attribute and for the companion. The Dokploy API returns most secrets on read, so the provider marks the secret in the private state of the resource and keeps the server’s value out of the state on every refresh. terraform show prints no secret.
  • The move between the two shapes is an in-place update. To go back to the plain attribute, remove the pair and set the plain attribute; the state holds the secret again.
  • A Computed secret (database_root_password) can also drop both: the server keeps its value, and the state takes it, as before the companions.
  • dokploy_vault_provider is the exception on the first rule. The Dokploy API masks each vault secret on read, so the provider cannot resend a stored value; it sends the companion’s value on every update. The version then only starts an update when nothing else changed.
  • terraform import has no companion flag. An imported resource holds the secret in its state until the first apply with the companion, which sends the value and removes it.
  • A write-only value needs Terraform 1.11 or later. An older CLI rejects it at validation with a message that names the version. A configuration without the companions works on Terraform 1.5 as before.

Backup destination credentials

dokploy_destination carries access_key and secret_access_key, because the user who creates the record must supply them. Both have a write-only companion, access_key_wo and secret_access_key_wo.

The data source omits both on purpose. The Dokploy endpoint destination.one returns them in cleartext. A data source exists for references, and a copy of the credentials of a shared backup target in the state of each consumer widens their exposure with no gain. A consumer needs only the id:

hcl
data "dokploy_destination" "backups" {
  name = "s3-backups"
}

resource "dokploy_backup" "db" {
  destination_id  = data.dokploy_destination.backups.id
  service_id      = dokploy_postgres.db.id
  service_type    = "postgres"
  cron_expression = "0 3 * * *"
  database        = "app"
  prefix          = "backups/app/"
}

All six dokploy_backup attributes in this example are required, prefix included.

Compose backup credentials

A backup of a database inside a dokploy_compose service has no database record that Dokploy can read the credentials from, so the backup record carries them. dokploy_backup takes them in three attributes, and the engine in compose_database_type says which ones apply:

  • postgres: compose_database_user.
  • mariadb and mongo: compose_database_user and compose_database_password.
  • mysql: compose_database_root_password.

The two passwords have write-only companions, compose_database_password_wo and compose_database_root_password_wo. The Dokploy endpoint backup.one returns both passwords in cleartext, so the companions follow the rules above: the provider keeps the server value out of the state, and an update of another attribute resends the stored password, because the Dokploy update endpoint replaces the credentials as a whole.

hcl
resource "dokploy_backup" "nextcloud_db" {
  service_id            = dokploy_compose.nextcloud.id
  service_type          = "compose"
  compose_database_type = "mariadb"
  service_name          = "nextcloud-db"
  database              = "nextcloud"
  prefix                = "/nextcloud"
  cron_expression       = "0 0 * * *"
  destination_id        = data.dokploy_destination.backups.id

  compose_database_user                = "nextcloud"
  compose_database_password_wo         = var.nextcloud_db_password
  compose_database_password_wo_version = 1
}

Verify a connection before the write

The resources that hold a credential for an outside service take the optional verify_connection attribute:

  • dokploy_registry, dokploy_destination, and dokploy_ai.
  • dokploy_gitlab_provider, dokploy_bitbucket_provider, and dokploy_gitea_provider.
  • The twelve dokploy_<channel>_notification resources.

Set verify_connection = true to run the Dokploy test endpoint of the resource. The attribute exists only in Terraform. null and false both skip the test, so a configuration without the attribute plans no change. terraform import leaves the attribute null.

What the provider sends depends on the resource:

  • Registry, destination, AI, and notification: the test runs before the write and carries the credentials of the configuration, which includes the value of a write-only companion. A failed test fails the apply with the message of the server, and the provider writes nothing.
  • GitLab, Bitbucket, and Gitea: the test sends only the id of the stored record, so it runs after the write. A failed create taints the resource, and the record stays on the server. A failed update keeps the new values in the state.

The provider removes the credentials from each error message. The test of a destination lists the bucket with rclone, and Dokploy puts the command line with both keys in its error message. The provider redacts both keys.

Four cases need care:

  • Gitea: the test needs an OAuth access token. The token exists only after a person authorizes the application in the Dokploy UI. A test on the first apply always fails, so do not set verify_connection on a new dokploy_gitea_provider.
  • Notifications: every test sends a real message to the channel. Each apply with verify_connection = true that changes the resource sends it again.
  • Telegram and Lark: the Dokploy test answers true for a wrong bot token (Telegram) and for an unreachable URL (Lark). A pass does not prove the credentials.
  • Registry: Dokploy already runs docker login when it stores the record, so the attribute adds an early check only.
hcl
resource "dokploy_slack_notification" "deploys" {
  name                   = "deploys"
  webhook_url_wo         = var.slack_webhook_url
  webhook_url_wo_version = 1
  app_deploy             = true

  verify_connection = true
}

The state holds these values in cleartext

The Terraform state stores attribute values in cleartext, sensitive values included. That is the design of Terraform, not a property of this provider. The Dokploy API returns database passwords and destination credentials, so a plain secret attribute holds the real value in the state, not a one-way hash. The write-only companions are the way out: with them, the state holds null for the secret.

Use a remote state with encryption at rest, and restrict who can read it, with or without the companions: env and the non-secret attributes are still there.

The same applies to dogfood/dry-run.sh. It writes a throwaway terraform.tfstate into dogfood/scratch/ and keeps it on failure for inspection. That directory is gitignored, but it is real state on disk.