basecamp/kamal · error · Kamal::ConfigurationError

Local registry with remote builder requires an SSH URL (e.g.

Error message

Local registry with remote builder requires an SSH URL (e.g., ssh://user@host)

What it means

Kamal::Configuration#ensure_local_registry_remote_builder_has_ssh_url fires when the registry is local (registry configured with no server) while the builder is remote, and the remote builder URL's URI scheme is not ssh. A local registry is only reachable from machines on that network, so a remote builder must connect over SSH (e.g. ssh://user@host) for the push/pull to work; http/tcp URLs would point the builder at a registry it cannot reach or secure.

Source

Thrown at lib/kamal/configuration.rb:411

    def ensure_one_host_for_ssl_roles
      roles.each(&:ensure_one_host_for_ssl)

      true
    end

    def ensure_unique_hosts_for_ssl_roles
      hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
      duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }

      raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?

      true
    end

    def ensure_local_registry_remote_builder_has_ssh_url
      if registry.local? && builder.remote?
        unless URI(builder.remote).scheme == "ssh"
          raise Kamal::ConfigurationError, "Local registry with remote builder requires an SSH URL (e.g., ssh://user@host)"
        end
      end

      true
    end

    def ensure_no_conflicting_proxy_runs
      all_hosts.each do |host|
        run_configs = proxy_runs(host)
        if run_configs.uniq.size > 1
          raise Kamal::ConfigurationError, "Conflicting proxy run configurations for host #{host}"
        end
      end
    end

    def proxy_runs(host)
      (host_roles(host) + host_accessories(host)).map(&:proxy).compact.map(&:run).compact
    end

View on GitHub (pinned to eee0083b38)

Solutions

  1. Change the builder remote to an SSH URL: `builder: { remote: ssh://user@buildhost }`.
  2. Ensure the deploying user has SSH key access to the build host (test with `ssh user@buildhost docker version`).
  3. If the builder need not be remote, remove the `remote:` key so building happens locally against the local registry.

Example fix

# config/deploy.yml — before
registry:
  username: deploy
builder:
  remote: buildhost.example.com

# after
registry:
  username: deploy
builder:
  remote: ssh://deploy@buildhost.example.com
Defensive patterns

Strategy: validation

Validate before calling

require "uri"

def local_registry_remote_builder_ok?(raw)
  registry_local = raw.dig("registry", "server").nil?
  remote = raw.dig("builder", "remote")
  return true unless registry_local && remote
  uri = URI.parse(remote.to_s)
  uri.scheme == "ssh" || uri.scheme.nil? && remote.include?("@") ? uri.scheme == "ssh" : false
rescue URI::InvalidURIError
  false
end

Try / catch

begin
  config = Kamal::Configuration.new(create_config_files: false)
rescue Kamal::ConfigurationError => e
  puts "Deploy config invalid: #{e.message}"
  exit 1
end

Prevention

When it happens

Trigger: A `registry:` block with no server (local registry) plus `builder: { remote: tcp://... }` or a plain hostname like `remote: buildhost.example.com` that parses to a nil scheme; switching registry to a local one but leaving an old docker-context-style remote builder URL; following a local-registry tutorial and pasting a bare hostname.

Common situations: Self-hosted registry on the deploy host with a remote build machine; migrating from docker context strings (which lack ssh://) to kamal 2 builder config; typos like `ssh//user@host` that parse to a nil scheme.

Related errors


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