{"record":{"id":"eb6e44eaa2b2aff7","repo":"basecamp/kamal","slug":"ssl-is-only-supported-on-a-single-server-unless-yo","errorCode":null,"errorMessage":"SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}","messagePattern":"SSL is only supported on a single server unless you provide custom certificates, found #(.+?) servers for role #(.+?)","errorType":"exception","errorClass":"Kamal::ConfigurationError","httpStatus":null,"severity":"error","filePath":"lib/kamal/configuration/role.rb","lineNumber":162,"sourceCode":"        host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options\n    end\n  end\n\n  def asset_path_options\n    asset_path_config&.dig(1)\n  end\n\n  def asset_extracted_directory(version = config.version)\n    File.join config.assets_directory, \"extracted\", [ name, version ].join(\"-\")\n  end\n\n  def asset_volume_directory(version = config.version)\n    File.join config.assets_directory, \"volumes\", [ name, version ].join(\"-\")\n  end\n\n  def ensure_one_host_for_ssl\n    if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?\n      raise Kamal::ConfigurationError, \"SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}\"\n    end\n  end\n\n  private\n    def initialize_specialized_proxy\n      proxy_specializations = specializations[\"proxy\"]\n\n      if primary?\n        # only false means no proxy for non-primary roles\n        @running_proxy = proxy_specializations != false\n      else\n        # false and nil both mean no proxy for non-primary roles\n        @running_proxy = !!proxy_specializations\n      end\n\n      if running_proxy?\n        proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations\n","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/basecamp/kamal/blob/eee0083b38661c3707c6b6052cc89e85038a096c/lib/kamal/configuration/role.rb#L144-L180","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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)","Reduce the role to a single host (hosts: [ one.ip ]) and keep automatic SSL","Terminate SSL upstream (ALB/CloudFlare/CDN) and set proxy: ssl: false","Split traffic across multiple single-host roles (e.g. web and web2), each with its own automatic cert"],"exampleFix":"# deploy.yml (before)\nweb:\n  hosts:\n    - 1.1.1.1\n    - 2.2.2.2\nproxy:\n  ssl: true\n\n# deploy.yml (after)\nweb:\n  hosts:\n    - 1.1.1.1\n    - 2.2.2.2\nproxy:\n  ssl: true\n  ssl_certificate: /etc/ssl/myapp.crt\n  ssl_private_key: /etc/ssl/myapp.key","handlingStrategy":"validation","validationCode":"# Pre-flight before deploying: fail fast with your own message\nrequire \"yaml\"\n\ndef ssl_multi_host?(deploy_yml = \"config/deploy.yml\")\n  config = YAML.load_file(deploy_yml, aliases: true)\n  proxy = config.dig(\"proxy\") || {}\n  return false unless proxy[\"ssl\"]\n  return false if proxy[\"ssl_certificate\"] && proxy[\"ssl_private_key\"]\n\n  config.fetch(\"servers\", {}).any? do |_role, spec|\n    hosts = spec.is_a?(Hash) ? spec[\"hosts\"] : spec\n    Array(hosts).size > 1\n  end\nend\n\nabort \"multi-host role with automatic SSL will fail kamal config\" if ssl_multi_host?","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Decide the TLS strategy before scaling past one host: custom certs or terminate SSL upstream","Keep certificate/key paths in deploy.yml under proxy: so custom_ssl_certificate? stays true when hosts grow","Run kamal config in CI to catch this at pull-request time, not deploy time"],"tags":["kamal","ssl","proxy","configuration","deployment"],"backgroundTag":"ssl-certificate-misconfiguration","analyzedSha":"eee0083b38661c3707c6b6052cc89e85038a096c","analyzedAt":"2026-08-21T15:17:22.045Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}