Skip to main content

Why Self-Hosted PaaS Secrets Managers Still Default to Plaintext Env Vars: A Survey of Coolify, Dokploy, and CapRover

11 min readDora NodaDora Noda
Share
On this page

GitGuardian counted 29 million new hardcoded secrets hitting public GitHub in 2025 — a 34% jump year over year, the largest single-year increase ever recorded. Meanwhile, the tools many of us trust to run production apps on our own servers keep those same secrets one docker inspect away from anyone with daemon access. If you self-host a PaaS like Coolify, Dokploy, or CapRover, your database password's protection depends far more on which dashboard you picked than most teams realize.

Here is the survey, up front. This is how the three most popular single-box self-hosted PaaS tools handle app secrets today, next to a Sealed Secrets / External Secrets Operator (ESO) pattern on Kubernetes:

PropertyCoolifyDokployCapRoverSealed Secrets / ESO on K8s
Encrypted at rest in the control-plane DBPartial — sensitive vars encrypted; build vars plaintextNo — plaintext in DBNo — plaintextYes, with etcd EncryptionConfiguration/KMS (must be enabled)
Dashboard read-back of secret valuesWrite-only if marked sensitiveFully visibleFully visible, returned to clientNo read-back path; secret-of-record external or sealed
Safe to commit config to gitNoNoNoYes — SealedSecret CRs or ExternalSecret references
Key-management boundary outside the app hostNoNoNoYes — cluster private key or external vault (Vault, AWS SM, …)
Known exposure incidents / open issuesDeployment logs leaked env vars (#7019)Secure-env still an open request (#2817)Plaintext secrets acknowledged since #616CVE surface in controllers; misconfig risk
Rotation storyEdit + redeploy per appEdit + redeploy per appEdit + redeploy per appRotate in provider; ESO re-syncs on interval

The one-paragraph read: none of the three single-box tools gives you a key-management boundary separate from the machine that runs your apps, two of the three store secrets in plaintext outright, and all three treat the dashboard as a trusted reader of every value. The Kubernetes pattern is not automatically better — vanilla Kubernetes Secrets are just base64 in etcd until you turn on encryption — but it is the only one of the four columns where "safe to commit to git" and "keys live somewhere else" are even possible. The rest of this post walks through the receipts, why the single-box tools ended up here (it's architecture, not laziness), and what the harder path actually buys.

The Single-Box Survey, Tool by Tool

Coolify: the best of the three, with sharp edges

Coolify is the most secrets-conscious of the group. Environment variables you mark as sensitive are encrypted before being stored in Coolify's database, and a sensitive value becomes write-only — it won't be shown again in the UI after you save it. That's a genuine step up from its peers.

But two edges cut against the headline. First, build variables are stored in plaintext, while only regular runtime variables get encryption — so a token your Dockerfile needs at build time sits unprotected in the same database. Second, exposure has happened downstream of storage: issue #7019 documents deployment logs printing all environment variables from .env files in plaintext — API keys, tokens, database credentials — to anyone who could read the deploy log. Encrypting the database row doesn't help when the deploy pipeline echoes the decrypted value into a log page.

To its credit, Coolify supports Docker BuildKit build secrets, which inject build-time values without baking them into image layers — they don't show up in docker history. The primitives are improving; the defaults and the log surfaces are where teams get burned.

Dokploy: plaintext, and the maintainers know it

Dokploy stores all environment variables — database passwords, API keys, tokens — as plaintext in its database, and displays them in the dashboard to anyone with access. This isn't a hidden flaw; it's tracked openly. Issue #2817 requests secure environment support for compose and application resources, and issue #3821 asks for integration with external secret managers like Vault, AWS Secrets Manager, and Doppler — currently there is no way to use any of them. A proposed "secure mode" would encrypt values for UI display, but as of mid-2026 the shipped reality is: whoever can read the Dokploy dashboard or its Postgres database can read every secret of every app it manages.

CapRover: the oldest issue in the survey

CapRover's situation is the same, and has been acknowledged the longest. Apps and databases that need secrets get them as plaintext environment variables, and those values are returned to the client in the CapRover dashboard. Issue #616 — asking for real secret management — has been open for years. CapRover runs on Docker Swarm, which ironically ships a better primitive: Swarm secrets are mounted as files on an in-memory tmpfs at /run/secrets/, never exposed as environment variables and never committed into an image. The platform sits on top of a runtime that can do this, and still hands your apps plaintext env vars.

Why Plaintext Is the Default: It's Architecture, Not Laziness

It's tempting to read this survey as three teams that didn't care. The truth is more structural, and it explains why the pattern persists across otherwise very different codebases.

A single-box PaaS has no key-management boundary. The dashboard that accepts your secret, the database that stores it, and the runtime that injects it into your container are the same process on the same host. Encrypting the database row buys surprisingly little in that topology: the decryption key must live on the same machine, readable by the same process, so any attacker (or log line, or backup job) with host access gets both the ciphertext and the key. Coolify's partial encryption is real defense-in-depth against casual database dumps — but it cannot be a boundary, because there's nothing on the other side of it.

Env vars are the universal contract. The 12-factor convention made environment variables the lingua franca of app configuration, and every buildpack, every framework's config loader, every docker compose file expects them. A PaaS that wants "push a repo and it just works" across thousands of unknown apps almost has to speak env vars at the boundary. The alternative — file-mounted secrets like Swarm's /run/secrets/ — requires apps to read files from a path, which most off-the-shelf apps won't do without modification.

And env vars leak by design. They're visible via docker inspect to anyone with daemon access, appear in process listings, are inherited by every child process, and get swept up by crash handlers and log aggregators. Environment-variable leakage through log pipelines has triggered real compliance violations in regulated environments. GitGuardian's 2026 report adds a sobering angle: 32.2% of internal repositories contain at least one hardcoded secret — nearly six times the rate of public repos — precisely because "it's internal" feels safe, the same instinct that makes a plaintext dashboard on your own server feel fine. And 70% of secrets leaked in 2022 were still active years later; a value that leaks through a deploy log tends to stay valid.

So: plaintext env vars aren't the default because self-hosted PaaS authors are sloppy. They're the default because the single-box architecture can't offer a real boundary, and the ecosystem's configuration contract actively rewards the leakiest injection mechanism.

What Sealed Secrets and ESO Actually Buy

The Kubernetes answer to this is not "Kubernetes Secrets." It's one of two patterns layered on top, each of which creates exactly the boundary the single-box tools lack.

Sealed Secrets: encrypt in git, decrypt only in the cluster

Bitnami's Sealed Secrets is a controller plus a CLI (kubeseal) built on asymmetric crypto. The controller generates a keypair; the private key never leaves the cluster. You encrypt locally with the public key:

bash
kubectl create secret generic db-creds \
  --from-literal=DATABASE_URL='postgres://app:s3cr3t@db:5432/prod' \
  --dry-run=client -o yaml | kubeseal -o yaml > db-creds-sealed.yaml

The output is a SealedSecret custom resource:

yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-creds
  namespace: prod
spec:
  encryptedData:
    DATABASE_URL: AgBy8hCi...   # ciphertext, safe to commit

That file goes straight into git — the whole point. Nobody with repo access can recover the plaintext; only the controller in your target cluster can decrypt it, at which point it materializes an ordinary Kubernetes Secret for your app to consume. Your deploy config becomes fully version-controlled, reviewable, and revertible, secrets included, with zero secrets in the repo.

External Secrets Operator: git holds references, a vault holds values

External Secrets Operator inverts the model: the secret-of-record lives in a dedicated store — HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Azure Key Vault, 1Password, Doppler, Infisical, among 45+ supported providers — and git holds only an ExternalSecret resource saying "sync key X from store Y into this namespace." The operator pulls values at runtime and keeps them synced on an interval, which is what makes rotation real: rotate the credential in Vault, and ESO propagates it without anyone touching the cluster or redeploying a dashboard.

ESO also answers the "is this maintained?" question better than it did a year ago. After a maintainer-shortage scare in spring 2025 paused releases, new corporate backing revived the project; it has since shipped a stable v1 API and, as of mid-2026, sits at v2.6.0.

The honest caveat: Kubernetes is not hygienic by default either

Skip this part and the comparison becomes cherry-picking. A vanilla Kubernetes Secret is base64-encoded, not encrypted — anyone with access to etcd, an etcd backup, or the etcd data directory can decode every secret in the cluster. To close that hole you must configure the API server with an EncryptionConfiguration, ideally using the KMS provider so the encryption key lives outside the cluster in AWS KMS, Cloud KMS, or similar. On a Cluster-API-provisioned cluster this is a deliberate step in your cluster template, not a default. The pattern buys you a boundary you can build — sealed data in git, keys in a KMS, values in a vault. It does not buy hygiene automatically.

What You Give Up With the Easy Tool

Put concretely, choosing the single-box PaaS over the Kubernetes pattern trades away five named properties:

  • An external key boundary. With Sealed Secrets + KMS-encrypted etcd, compromising the app host is no longer sufficient to decrypt everything. On a single-box PaaS, it always is.
  • Git-committable configuration. Your Coolify or Dokploy app config can never be fully committed, because the secret values only exist in the tool's database. SealedSecret and ExternalSecret files close the loop: the repo is the whole truth.
  • Rotation without ceremony. All three single-box tools rotate by edit-and-redeploy, per app, by hand. ESO rotates by changing the value in the provider once.
  • Least-privilege read-back. Every dashboard admin on Dokploy and CapRover can read every production credential. Vault-backed setups scope who can read what, and Coolify's write-only sensitive vars are the only single-box gesture toward this.
  • An audit trail. Vault and cloud secret managers log every read. A plaintext database row read by a dashboard process logs nothing.

Now the other side of the ledger, because it's real: the Kubernetes path costs you a Cluster API management cluster (or a managed control plane), a controller or operator to run and upgrade, a provider to configure, etcd encryption to enable, and the operational literacy to debug all of it at 2 a.m. A two-person team shipping a side project on a €50 Hetzner box can rationally look at that list and decline. If your threat model is "my co-founder and I are the only ones with dashboard access, and the box has no other tenants," Coolify with sensitive-marked vars is a defensible choice — provided you know that's the choice you made, and that your deploy logs are part of your secret perimeter.

The verdict, compressed: pick the single-box tool when dashboard-readers and host-admins are the same two people and the blast radius of a leaked credential is tolerable. Pick the Sealed-Secrets/ESO pattern the moment any of those stops being true — more operators than founders, compliance requirements, credentials whose leak is existential, or config you need in git.

The Gap in the Middle Is the Interesting Part

The real conclusion of this survey isn't "Kubernetes good, Coolify bad." It's that the market has split into two unsatisfying halves: tools with excellent push-to-deploy UX and no secret-hygiene boundary, and a platform with every boundary you could want and none of the UX. The open issues on all three single-box tools — secure-env modes, Vault integrations, secret managers — are users asking for the missing half. The next generation of self-hosted PaaS has to treat "your secrets are sealed, your config is committable, and your deploy is still one git push" as a single feature, not a trade-off.

Until then, run the survey table against your own setup. If your answer to "who can read production credentials?" includes a plaintext database row, a deploy log, or everyone with a dashboard login — you now know exactly which column you're living in.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on Kubernetes machines you own, with platform state that agents and operators can read without scraping a dashboard. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex