Authentication Secret Management#
BETTER_AUTH_SECRET is Dokploy's primary authentication secret. It serves two roles simultaneously: it is the signing key for session tokens and the fallback encryption key for all environment variables stored at rest (AES-256-GCM). Because it does both jobs, changing the secret without a migration step will silently empty every deployed service's runtime environment.
Secret Resolution Order#
At startup, auth-secret.ts resolves the auth secret in this order:
BETTER_AUTH_SECRETenv varBETTER_AUTH_SECRET_FILEDocker secret file- Hardcoded legacy fallback
"better-auth-secret-123456789"— triggers a deprecation banner and will be removed in a future release
Separately, encryption-secret.ts optionally resolves a dedicated ENCRYPTION_KEY / ENCRYPTION_KEY_FILE. When set, ENCRYPTION_KEY becomes the primary encryption key and BETTER_AUTH_SECRET is kept only as a decryption fallback — separating auth from encryption .
How the Encryption Keyring Is Built#
encryption.ts derives a 256-bit key at startup via HMAC-SHA256:
deriveKey(secret) = HMAC-SHA256(secret, "dokploy:db-encryption:v1")
The active decryption keyring contains :
| Scenario | Keys in keyring |
|---|---|
No ENCRYPTION_KEY set | [deriveKey(BETTER_AUTH_SECRET)] |
ENCRYPTION_KEY adopted | [deriveKey(ENCRYPTION_KEY), deriveKey(BETTER_AUTH_SECRET)] |
| Backup restored with key file | Above + keys from encryption.key at BASE_PATH |
Gap: The keyring has no slot for a previous BETTER_AUTH_SECRET. If the secret is replaced (e.g., migrating from the hardcoded default as the deprecation banner instructs), all values encrypted under the old key become undecryptable .
The Silent Empty-Environment Failure#
When decryption fails, the encryptedText Drizzle column type is fail-open: it logs a console.error and returns the raw ciphertext instead of throwing. That ciphertext (enc:v1:...) is then passed to dotenv.parse(), which returns {} because it matches no KEY=VALUE pattern. The service is deployed with zero environment variables, and the deployment status is still reported as done — no error surfaces in the UI or deployment log .
The only trace is a server-side console.error :
"Failed to decrypt an encrypted column; returning the raw value. This usually means BETTER_AUTH_SECRET changed after the value was encrypted."
2FA and Dashboard Infinite-Loading Symptoms#
A BETTER_AUTH_SECRET mismatch also breaks session verification. Reported symptoms include :
/api/auth/two-factor/verify-totprequest hangs forever (never returns)/api/auth/sign-in/emailhangs after 2FA records are altered- Dashboard stuck on infinite loading / spinner after upgrades
Fix: If you never changed the secret, restoring the original value (or the hardcoded default "better-auth-secret-123456789") and restarting Dokploy will re-align the keyring. If 2FA records are orphaned, reset them directly in Postgres :
DELETE FROM two_factor WHERE user_id = '<your_user_id>';
UPDATE "user" SET two_factor_enabled = false WHERE id = '<your_user_id>';
Cookie Security Note#
When IS_CLOUD is false (all self-hosted instances), session cookies are hardcoded with secure: false regardless of HTTPS configuration . This is a separate concern from the secret but means session tokens can be sent in plaintext if a user accesses via HTTP.
Migrating to a Proper Secret#
⚠️ Do not follow the startup banner's migration script without understanding the keyring gap described above. Until a fix lands (tracked in #4833), replacing
BETTER_AUTH_SECRETon a running instance will orphan all encrypted env vars.
Safe migration path (once the fix is in):
- Set
ENCRYPTION_KEY(via Docker secret) as the dedicated encryption key before rotatingBETTER_AUTH_SECRET. The keyring fallback for theENCRYPTION_KEYadoption path is already implemented . - Rotate
BETTER_AUTH_SECRETafter all env vars have been lazily re-encrypted underENCRYPTION_KEY. - Verify services still receive their environment variables before taking production traffic.
Key Source Files#
| File | Purpose |
|---|---|
packages/server/src/lib/auth-secret.ts | Resolves BETTER_AUTH_SECRET; defines and warns on legacy fallback |
packages/server/src/lib/encryption.ts | Key derivation, encryptValue, decryptValue, keyring management |
packages/server/src/lib/encryption-secret.ts | Resolves optional ENCRYPTION_KEY |
packages/server/src/db/schema/utils.ts | encryptedText Drizzle column type; fail-open decryption |