basecamp/kamal · error · Kamal::ConfigurationError

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

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.

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)

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.