basecamp/kamal · error · Kamal::ConfigurationError

SSL is only supported on a single server unless you provide

Error message

SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}

What it means

Kamal raises Kamal::ConfigurationError while loading config when a role that runs the proxy (a primary role without proxy: false) has SSL enabled and spans more than one host, and no custom certificate is configured. Kamal's built-in SSL uses kamal-proxy's automatic certificates, which only work for a single server per role, so multi-host roles must bring their own certificate material.

Source

Thrown at lib/kamal/configuration/role.rb:162

        host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
    end
  end

  def asset_path_options
    asset_path_config&.dig(1)
  end

  def asset_extracted_directory(version = config.version)
    File.join config.assets_directory, "extracted", [ name, version ].join("-")
  end

  def asset_volume_directory(version = config.version)
    File.join config.assets_directory, "volumes", [ name, version ].join("-")
  end

  def ensure_one_host_for_ssl
    if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
      raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
    end
  end

  private
    def initialize_specialized_proxy
      proxy_specializations = specializations["proxy"]

      if primary?
        # only false means no proxy for non-primary roles
        @running_proxy = proxy_specializations != false
      else
        # false and nil both mean no proxy for non-primary roles
        @running_proxy = !!proxy_specializations
      end

      if running_proxy?
        proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations

View on GitHub (pinned to eee0083b38)

Solutions

  1. Provide custom certificates: add ssl_certificate: /path/cert.pem and ssl_private_key: /path/key.pem under proxy: in deploy.yml (custom_ssl_certificate? then passes)
  2. Reduce the role to a single host (hosts: [ one.ip ]) and keep automatic SSL
  3. Terminate SSL upstream (ALB/CloudFlare/CDN) and set proxy: ssl: false
  4. Split traffic across multiple single-host roles (e.g. web and web2), each with its own automatic cert

Example fix

# deploy.yml (before)
web:
  hosts:
    - 1.1.1.1
    - 2.2.2.2
proxy:
  ssl: true

# deploy.yml (after)
web:
  hosts:
    - 1.1.1.1
    - 2.2.2.2
proxy:
  ssl: true
  ssl_certificate: /etc/ssl/myapp.crt
  ssl_private_key: /etc/ssl/myapp.key
Defensive patterns

Strategy: validation

Validate before calling

# Pre-flight before deploying: fail fast with your own message
require "yaml"

def ssl_multi_host?(deploy_yml = "config/deploy.yml")
  config = YAML.load_file(deploy_yml, aliases: true)
  proxy = config.dig("proxy") || {}
  return false unless proxy["ssl"]
  return false if proxy["ssl_certificate"] && proxy["ssl_private_key"]

  config.fetch("servers", {}).any? do |_role, spec|
    hosts = spec.is_a?(Hash) ? spec["hosts"] : spec
    Array(hosts).size > 1
  end
end

abort "multi-host role with automatic SSL will fail kamal config" if ssl_multi_host?

Prevention

When it happens

Trigger: deploy.yml with servers: web: hosts: [ip1, ip2] (hosts.size > 1) on the primary role together with proxy: ssl: true (and no ssl: false override) and no proxy.ssl_certificate / proxy.ssl_private_key. Raised from ensure_one_host_for_ssl during Kamal::Configuration load, so any kamal command that builds the config fails.

Common situations: Scaling a web role from one server to several while relying on Kamal's automatic Let's Encrypt certs; copying a single-host example config and adding hosts; upgrading Kamal versions where certificate option names changed; forgetting that only the primary role runs the proxy.

Understand the failure class

Related errors


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