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

  1. 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).
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21). Data as JSON: /api/errors/cde5845eaa3812dd. Report an issue: GitHub.