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:
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:
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-outcannot 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 thenullon 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.
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_versionchanges. 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 showprints 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
Computedsecret (database_root_password) can also drop both: the server keeps its value, and the state takes it, as before the companions. dokploy_vault_provideris 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 importhas 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:
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.mariadbandmongo:compose_database_userandcompose_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.
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, anddokploy_ai.dokploy_gitlab_provider,dokploy_bitbucket_provider, anddokploy_gitea_provider.- The twelve
dokploy_<channel>_notificationresources.
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_connectionon a newdokploy_gitea_provider. - Notifications: every test sends a real message to the channel.
Each apply with
verify_connection = truethat changes the resource sends it again. - Telegram and Lark: the Dokploy test answers
truefor a wrong bot token (Telegram) and for an unreachable URL (Lark). A pass does not prove the credentials. - Registry: Dokploy already runs
docker loginwhen it stores the record, so the attribute adds an early check only.
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.