basecamp/kamal · error · Kamal::ConfigurationError

Secret '# ' not found in #

Error message

Secret '#{key}' not found in #{secrets_files.join(", ")}

What it means

Kamal resolves ERb like <%= secrets.PASSWORD %> in deploy.yml through Kamal::Secrets, which Dotenv-parses .kamal/secrets-common and .kamal/secrets (or .kamal/secrets.<destination> when a destination is set). This Kamal::ConfigurationError means at least one secrets file exists, but none of them defines the requested key, so the lookup (secrets.fetch) raised KeyError and was converted.

Solutions

  1. Fetch or append the missing key: kamal secrets fetch -a <adapter> --account <acct> PASSWORD >> .kamal/secrets
  2. Or add it manually as KEY=value in .kamal/secrets (same format as dotenv)
  3. Verify the exact key names with kamal secrets print and compare against every <%= secrets.X %> reference in deploy.yml
  4. If using destinations, confirm the key lives in .kamal/secrets-common or the .kamal/secrets.<destination> you deploy with

Example fix

# error: Secret 'PASSWORD' not found in .kamal/secrets-common, .kamal/secrets

# fix (terminal)
kamal secrets fetch -a bitwarden-sm PASSWORD >> .kamal/secrets

# or (.kamal/secrets)
# before: (no PASSWORD line)
# after:
PASSWORD=correct-horse-battery-staple
Defensive patterns

Strategy: validation

Validate before calling

# Verify every <%= secrets.X %> reference in deploy.yml resolves before deploying
require "yaml"

deploy = File.read("config/deploy.yml")
referenced = deploy.scan(/secrets\.([A-Za-z0-9_]+)/).flatten.uniq

files = [".kamal/secrets-common", ".kamal/secrets", ".kamal/secrets.#{ENV["KAMAL_DESTINATION"]}"].compact
existing = files.select { |f| File.exist?(f) }
existing.each do |f|
  referenced -= File.readlines(f).map { |l| l[/\A([A-Za-z0-9_]+)=/, 1] }.compact
end

abort "unresolved secrets: #{referenced.join(", ")}" unless referenced.empty?

Prevention

When it happens

Trigger: deploy.yml references secrets.PASSWORD but no line PASSWORD=... exists in .kamal/secrets-common or the destination-specific file; the key was fetched under a different name; the key exists only in a different destination's file (only common + current destination are read).

Common situations: Adding a new env/secret reference to deploy.yml before running kamal secrets fetch; typo between deploy.yml and the secrets file; fetching secrets while a destination (-d staging) is active so they land in .kamal/secrets.staging but deploying without the destination.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at lib/kamal/secrets.rb:16

require "dotenv"

class Kamal::Secrets
  Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!

  def initialize(destination: nil, secrets_path: ".kamal/secrets")
    @destination = destination
    @secrets_path = secrets_path
    @mutex = Mutex.new
  end

  def [](key)
    synchronized_fetch(key)
  rescue KeyError
    if secrets_files.present?
      raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
    else
      raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
    end
  end

  def to_h
    secrets
  end

  def secrets_files
    @secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
  end

  def key?(key)
    synchronized_fetch(key).present?
  rescue KeyError
    false
  end

View on GitHub (pinned to eee0083b38)