basecamp/kamal · error · RuntimeError
Failed to login to Doppler
Error message
Failed to login to Doppler
What it means
Kamal's Doppler adapter raises this in #login when `doppler login -y` exits non-zero. The adapter first runs `doppler me --json 2> /dev/null` (loggedin?); if that probe fails it attempts an interactive browser login, which cannot complete in non-interactive shells. Note Doppler marks requires_account? false, so no --account option is needed — the failure is purely CLI authentication.
Source
Thrown at lib/kamal/secrets/adapters/doppler.rb:10
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
def requires_account?
false
end
private
def login(*)
unless loggedin?
`doppler login -y`
raise RuntimeError, "Failed to login to Doppler" unless $?.success?
end
end
def loggedin?
`doppler me --json 2> /dev/null`
$?.success?
end
def fetch_secrets(secrets, from:, **)
secrets = prefixed_secrets(secrets, from: from)
flags = secrets_get_flags(secrets)
secret_names = secrets.collect { |s| s.split("/").last }
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{flags}`
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
items = JSON.parse(items)View on GitHub (pinned to eee0083b38)
Solutions
- Set a Doppler service token so no interactive login is ever attempted: export DOPPLER_TOKEN=dp.st.xxxx (the adapter detects the dp.st prefix via service_token_set? and skips project flags/login needs).
- Or log in once interactively on the machine (`doppler login`) so `doppler me --json` succeeds and the adapter's loggedin? short-circuits the login path.
- If DOPPLER_TOKEN is set but not dp.st-prefixed, replace it with a service token created via `doppler configs tokens create` in the Doppler dashboard.
- In CI, prefer injecting DOPPLER_TOKEN as a masked variable rather than relying on cached sessions.
Example fix
# before: no session, interactive login fails in CI # -> RuntimeError: Failed to login to Doppler # after: use a service token (adapter treats dp.st.* as service token) export DOPPLER_TOKEN="dp.st.1.xxxx" kamal secrets pull
Defensive patterns
Strategy: try-catch
Validate before calling
require "open3"
def doppler_ready?
return true if ENV["DOPPLER_TOKEN"]&.start_with?("dp.st.") # service token: no interactive login
_out, _err, status = Open3.capture3("doppler", "me", "--json")
status.success?
end
abort "No Doppler session: set DOPPLER_TOKEN (dp.st.*) or run `doppler login`" unless doppler_ready? Try / catch
begin
secrets = adapter.fetch(%w[RAILS_MASTER_KEY])
rescue RuntimeError => e
if e.message == "Failed to login to Doppler"
raise "Doppler login failed (headless?): export DOPPLER_TOKEN=dp.st.* or pre-run `doppler login`"
end
raise
end Prevention
- Always inject a Doppler service token (dp.st.*) in CI instead of relying on interactive login.
- Probe `doppler me --json` in a pre-deploy step; mirror of the adapter's own loggedin? check with a clearer error.
- Never assume a local ~/.doppler session exists on fresh runners.
When it happens
Trigger: adapter.fetch(...) with no valid local Doppler session AND no usable service token: `doppler me --json` fails (not logged in, token invalid), then `doppler login -y` runs and fails because it needs a browser/TTY (CI, ssh without forwarding, Docker build) or the login flow errors out.
Common situations: CI pipelines or deploy containers with no doppler session persisted; DOPPLER_TOKEN set to a personal (dp.pt...) or CLI token instead of a service token so `doppler me` still fails; expired local token in ~/.doppler; running kamal over a non-interactive ssh session.
Related errors
- Could not authenticate to Bitwarden Secrets Manager. Did you
- Could not read #{secrets} from Doppler
- could not login to gcloud
- Failed to login to LastPass
- Failed to login to Passbolt
AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21).
Data as JSON: /api/errors/cde5845eaa3812dd.
Report an issue: GitHub.