dash 2.12.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/app/boot.rb +47 -11
  3. data/lib/kamal/cli/app/rollout_boot.rb +59 -0
  4. data/lib/kamal/cli/app/ssl_certificates.rb +12 -3
  5. data/lib/kamal/cli/app.rb +74 -7
  6. data/lib/kamal/cli/base.rb +41 -32
  7. data/lib/kamal/cli/doctor/config_checks.rb +28 -0
  8. data/lib/kamal/cli/doctor/endpoint_checks.rb +114 -0
  9. data/lib/kamal/cli/doctor/host_checks.rb +178 -0
  10. data/lib/kamal/cli/doctor.rb +112 -0
  11. data/lib/kamal/cli/healthcheck/drift_error.rb +7 -0
  12. data/lib/kamal/cli/healthcheck/poller.rb +60 -10
  13. data/lib/kamal/cli/main.rb +63 -0
  14. data/lib/kamal/cli/proxy/drift.rb +39 -0
  15. data/lib/kamal/cli/proxy/loadbalancer_claim.rb +70 -0
  16. data/lib/kamal/cli/proxy/loadbalancer_reboot.rb +87 -0
  17. data/lib/kamal/cli/proxy/reboot.rb +173 -0
  18. data/lib/kamal/cli/proxy.rb +201 -34
  19. data/lib/kamal/cli/prune.rb +7 -4
  20. data/lib/kamal/cli/templates/sample_hooks/post-app-stop.sample +9 -0
  21. data/lib/kamal/cli/templates/sample_hooks/post-proxy-deploy.sample +3 -0
  22. data/lib/kamal/cli/templates/sample_hooks/pre-app-stop.sample +12 -0
  23. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-deploy.sample +3 -0
  24. data/lib/kamal/cli.rb +1 -0
  25. data/lib/kamal/commands/app/proxy.rb +12 -0
  26. data/lib/kamal/commands/app.rb +8 -0
  27. data/lib/kamal/commands/base.rb +11 -1
  28. data/lib/kamal/commands/docker.rb +5 -0
  29. data/lib/kamal/commands/loadbalancer.rb +76 -34
  30. data/lib/kamal/commands/proxy.rb +111 -11
  31. data/lib/kamal/commands/prune.rb +16 -2
  32. data/lib/kamal/commands/server.rb +5 -0
  33. data/lib/kamal/configuration/accessory.rb +9 -8
  34. data/lib/kamal/configuration/boot.rb +38 -7
  35. data/lib/kamal/configuration/docs/accessory.yml +28 -1
  36. data/lib/kamal/configuration/docs/boot.yml +17 -2
  37. data/lib/kamal/configuration/docs/configuration.yml +2 -1
  38. data/lib/kamal/configuration/docs/proxy.yml +844 -24
  39. data/lib/kamal/configuration/docs/role.yml +86 -0
  40. data/lib/kamal/configuration/loadbalancer.rb +81 -6
  41. data/lib/kamal/configuration/proxy/acme.rb +69 -0
  42. data/lib/kamal/configuration/proxy/run.rb +194 -5
  43. data/lib/kamal/configuration/proxy.rb +495 -18
  44. data/lib/kamal/configuration/role/healthcheck.rb +95 -0
  45. data/lib/kamal/configuration/role.rb +104 -1
  46. data/lib/kamal/configuration/validator/proxy.rb +623 -10
  47. data/lib/kamal/configuration/validator/role.rb +26 -0
  48. data/lib/kamal/configuration/validator.rb +26 -3
  49. data/lib/kamal/configuration.rb +197 -0
  50. data/lib/kamal/sshkit_with_ext.rb +27 -2
  51. data/lib/kamal/utils.rb +22 -2
  52. data/lib/kamal/version.rb +1 -1
  53. data/lib/kamal.rb +11 -0
  54. metadata +18 -2
@@ -9,17 +9,18 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
9
9
  end
10
10
 
11
11
  def run
12
- pipe \
13
- [ :echo, proxy_image ],
14
- xargs(docker(:run,
15
- "--name", container_name,
16
- "--network", "kamal",
17
- "--detach",
18
- "--restart", "unless-stopped",
19
- "--publish", "80:80",
20
- "--publish", "443:443",
21
- "--label", label,
22
- *volume_mounts))
12
+ docker \
13
+ :run,
14
+ "--name", container_name,
15
+ "--network", "kamal",
16
+ "--detach",
17
+ "--restart", "unless-stopped",
18
+ "--label", label,
19
+ "--label", "#{Kamal::Commands::Proxy::CONFIG_DIGEST_LABEL}=#{loadbalancer_config.run_config_digest}",
20
+ *config_volume,
21
+ *run_args,
22
+ *loadbalancer_config.run.image,
23
+ *loadbalancer_config.run.run_command
23
24
  end
24
25
 
25
26
  def start
@@ -35,16 +36,35 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
35
36
  end
36
37
 
37
38
  def deploy(targets: [])
38
- target_args = targets.map { |t| "#{t}:80" }
39
+ docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service,
40
+ *loadbalancer_config.deploy_command_args(targets: targets)
41
+ end
42
+
43
+ def domains(subcommand)
44
+ docker :exec, container_name, "kamal-proxy", "domains", subcommand
45
+ end
46
+
47
+ def list(json: false)
48
+ docker :exec, container_name, "kamal-proxy", :list, *("--json" if json)
49
+ end
39
50
 
40
- hosts = loadbalancer_config.hosts
51
+ # Cache policy is edge-only under load balancing (see the layering contract),
52
+ # so the cache admin surface lives here - registered under the bare service
53
+ # name, unlike the per-role services on the proxy hosts.
54
+ def cache_stats(count: false, json: false)
55
+ docker :exec, container_name, "kamal-proxy", :cache, :stats, *optionize({ count: count || nil, json: json || nil }.compact)
56
+ end
41
57
 
42
- options = []
43
- options << "--target=#{target_args.join(',')}"
44
- options << "--host=#{hosts.join(',')}"
45
- options << "--tls" if loadbalancer_config.ssl?
58
+ def cache_purge(service, path_prefix: nil)
59
+ docker :exec, container_name, "kamal-proxy", :cache, :purge, service, *optionize({ "path-prefix": path_prefix }.compact)
60
+ end
61
+
62
+ def config_digest
63
+ docker :inspect, container_name, "--format", "'{{ index .Config.Labels \"#{Kamal::Commands::Proxy::CONFIG_DIGEST_LABEL}\" }}'"
64
+ end
46
65
 
47
- docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service, *options
66
+ def container_id
67
+ container_id_for(container_name: container_name)
48
68
  end
49
69
 
50
70
  def info
@@ -70,22 +90,49 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
70
90
  ).join(" "), host: host
71
91
  end
72
92
 
93
+ # Prune by the label the container was actually created with - on a shared
94
+ # proxy host that is the kamal-proxy title, and pruning by the loadbalancer
95
+ # title would leave the container behind for `run` to collide with.
73
96
  def remove_container
74
- docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-loadbalancer"
97
+ docker :container, :prune, "--force", "--filter", "label=#{label}"
75
98
  end
76
99
 
100
+ # Image label filters match labels baked into the image, and the load balancer
101
+ # runs the kamal-proxy image whichever host it sits on.
77
102
  def remove_image
78
- docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-loadbalancer"
103
+ docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
79
104
  end
80
105
 
81
106
  def ensure_directory
82
107
  make_directory loadbalancer_config.directory
83
108
  end
84
109
 
110
+ # Where the proxy secrets env file lands (see Proxy::Run#secrets_path) -
111
+ # the same .kamal/proxy directory the per-app proxy hosts use.
112
+ def ensure_proxy_directory
113
+ make_directory loadbalancer_config.run.host_directory
114
+ end
115
+
116
+ def remove_proxy_secrets_file
117
+ remove_file loadbalancer_config.run.secrets_path
118
+ end
119
+
85
120
  def ensure_apps_config_directory
86
121
  make_directory config.proxy_boot.apps_directory
87
122
  end
88
123
 
124
+ def ensure_services_directory
125
+ make_directory loadbalancer_config.services_directory
126
+ end
127
+
128
+ def read_service_owner
129
+ read_file loadbalancer_config.service_owner_file
130
+ end
131
+
132
+ def read_run_config_record
133
+ read_file loadbalancer_config.run_config_file
134
+ end
135
+
89
136
  def remove_directory
90
137
  super(loadbalancer_config.directory)
91
138
  end
@@ -95,11 +142,8 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
95
142
  end
96
143
 
97
144
  private
98
- def proxy_image
99
- [
100
- loadbalancer_config.config.proxy_boot.image_default,
101
- Kamal::Configuration::Proxy::Run::MINIMUM_VERSION
102
- ].join(":")
145
+ def run_args
146
+ loadbalancer_config.run_args
103
147
  end
104
148
 
105
149
  def on_proxy_host?
@@ -114,17 +158,15 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
114
158
  end
115
159
  end
116
160
 
117
- def volume_mounts
161
+ # kamal-proxy keeps its state under /home/kamal-proxy/.config/kamal-proxy
162
+ # whichever host it runs on - only the volume name differs, so a dedicated
163
+ # load balancer and a shared proxy host never fight over the same volume.
164
+ # (The apps-config mount comes with run_args, via the proxy's run surface.)
165
+ def config_volume
118
166
  if on_proxy_host?
119
- # When on a proxy host, use same volume mounts as proxy for app deployments
120
- [
121
- "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
122
- "--volume", "$PWD/#{config.proxy_boot.apps_directory}:/home/kamal-proxy/.apps-config"
123
- ]
167
+ [ "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy" ]
124
168
  else
125
- [
126
- "--volume", "kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer"
127
- ]
169
+ [ "--volume", "kamal-loadbalancer-config:/home/kamal-proxy/.config/kamal-proxy" ]
128
170
  end
129
171
  end
130
172
  end
@@ -2,25 +2,28 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
2
2
  delegate :argumentize, :optionize, to: Kamal::Utils
3
3
  attr_reader :proxy_run_config
4
4
 
5
+ CONFIG_DIGEST_LABEL = "org.kamal.proxy-config-digest"
6
+
5
7
  def initialize(config, host:)
6
8
  super(config)
7
9
  @proxy_run_config = config.proxy_run(host)
8
10
  end
9
11
 
10
- def run
12
+ def run(digest: nil, name: nil)
11
13
  if proxy_run_config
12
14
  docker \
13
15
  :run,
14
- "--name", container_name,
15
- "--network", "kamal",
16
+ "--name", name || container_name,
17
+ *proxy_run_config.network_args,
16
18
  "--detach",
17
19
  "--restart", "unless-stopped",
18
20
  "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
21
+ *config_digest_label_args(digest),
19
22
  *proxy_run_config.docker_options_args,
20
23
  *proxy_run_config.image,
21
24
  *proxy_run_config.run_command
22
25
  else
23
- pipe boot_config, xargs(docker_run)
26
+ pipe boot_config, xargs(docker_run(digest: digest))
24
27
  end
25
28
  end
26
29
 
@@ -28,12 +31,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
28
31
  docker :container, :start, container_name
29
32
  end
30
33
 
31
- def stop(name: container_name)
32
- docker :container, :stop, name
34
+ def stop(name: container_name, timeout: nil)
35
+ docker :container, :stop, *("--time #{timeout}" if timeout), name
33
36
  end
34
37
 
35
- def start_or_run
36
- combine start, run, by: "||"
38
+ def start_or_run(digest: nil)
39
+ combine start, run(digest: digest), by: "||"
37
40
  end
38
41
 
39
42
  def info
@@ -46,6 +49,92 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
46
49
  [ :awk, "-F:", "'{print \$NF}'" ]
47
50
  end
48
51
 
52
+ def config_digest
53
+ docker :inspect, container_name, "--format", "'{{ index .Config.Labels \"#{CONFIG_DIGEST_LABEL}\" }}'"
54
+ end
55
+
56
+ def container_id(only_running: false)
57
+ container_id_for(container_name: container_name, only_running: only_running)
58
+ end
59
+
60
+ def pull
61
+ if proxy_run_config
62
+ docker :pull, proxy_run_config.image
63
+ else
64
+ docker :pull, "#{substitute(read_image)}:#{substitute(read_image_version)}"
65
+ end
66
+ end
67
+
68
+ def list(name: container_name, json: false)
69
+ docker :exec, name, "kamal-proxy", :list, *("--json" if json)
70
+ end
71
+
72
+ def cache_stats(count: false, json: false)
73
+ docker :exec, container_name, "kamal-proxy", :cache, :stats, *optionize({ count: count || nil, json: json || nil }.compact)
74
+ end
75
+
76
+ def cache_purge(service, path_prefix: nil)
77
+ docker :exec, container_name, "kamal-proxy", :cache, :purge, service, *optionize({ "path-prefix": path_prefix }.compact)
78
+ end
79
+
80
+ # One mount destination per line - what the running container was actually
81
+ # booted with, as opposed to what the current configuration would mount.
82
+ def mount_destinations
83
+ docker :inspect, container_name, "--format", "'{{range .Mounts}}{{println .Destination}}{{end}}'"
84
+ end
85
+
86
+ # Zero-downtime handoff commands (proxy/run port_holder mode)
87
+
88
+ def port_holder?
89
+ proxy_run_config&.port_holder? || false
90
+ end
91
+
92
+ def next_container_name
93
+ "#{container_name}-next"
94
+ end
95
+
96
+ def run_holder
97
+ docker \
98
+ :run,
99
+ "--name", proxy_run_config.holder_container_name,
100
+ "--network", "kamal",
101
+ "--detach",
102
+ "--restart", "unless-stopped",
103
+ *proxy_run_config.holder_docker_args,
104
+ *proxy_run_config.image,
105
+ "kamal-proxy", "hold"
106
+ end
107
+
108
+ def start_holder_or_run
109
+ combine docker(:container, :start, proxy_run_config.holder_container_name), run_holder, by: "||"
110
+ end
111
+
112
+ def holder_container_id
113
+ container_id_for(container_name: proxy_run_config.holder_container_name, only_running: true)
114
+ end
115
+
116
+ # Cancel the restart policy before draining: drain makes the proxy exit on
117
+ # its own, which - unlike `docker stop` - an active restart policy would undo.
118
+ def disable_restart
119
+ docker :update, "--restart=no", container_name
120
+ end
121
+
122
+ def drain(timeout: nil)
123
+ docker :exec, container_name, "kamal-proxy", :drain, *("--drain-timeout=#{timeout}s" if timeout)
124
+ end
125
+
126
+ def wait_for_exit(name: container_name)
127
+ docker :wait, name
128
+ end
129
+
130
+ def remove_stopped_container(name: container_name)
131
+ docker :container, :rm, name
132
+ end
133
+
134
+ def promote_next_container
135
+ docker :container, :rename, next_container_name, container_name
136
+ end
137
+
49
138
  def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
50
139
  pipe \
51
140
  docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
@@ -59,6 +148,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
59
148
  ).join(" "), host: host
60
149
  end
61
150
 
151
+ def domains(subcommand)
152
+ docker :exec, container_name, "kamal-proxy", "domains", subcommand
153
+ end
154
+
62
155
  def remove_container
63
156
  docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
64
157
  end
@@ -88,6 +181,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
88
181
  make_directory config.proxy_boot.apps_directory
89
182
  end
90
183
 
184
+ # Static path rather than proxy_run_config.secrets_path: the file must be
185
+ # removable precisely when the run config (or its secrets) is gone.
186
+ def remove_proxy_secrets_file
187
+ remove_file File.join(config.proxy_boot.host_directory, Kamal::Configuration::Proxy::Run::SECRETS_FILENAME)
188
+ end
189
+
91
190
  def boot_config
92
191
  [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
93
192
  end
@@ -133,11 +232,11 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
133
232
  config.proxy_boot.container_name
134
233
  end
135
234
 
136
- def read_file(file, default: nil)
137
- combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
235
+ def config_digest_label_args(digest)
236
+ [ "--label", "#{CONFIG_DIGEST_LABEL}=#{digest}" ] if digest
138
237
  end
139
238
 
140
- def docker_run
239
+ def docker_run(digest: nil)
141
240
  docker \
142
241
  :run,
143
242
  "--name", container_name,
@@ -145,6 +244,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
145
244
  "--detach",
146
245
  "--restart", "unless-stopped",
147
246
  "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
247
+ *config_digest_label_args(digest),
148
248
  *config.proxy_boot.apps_volume.docker_args
149
249
  end
150
250
  end
@@ -13,9 +13,15 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
13
13
  "while read image tag; do docker rmi $tag; done"
14
14
  end
15
15
 
16
- def app_containers(retain:)
16
+ # Scoped to one role so a busy sibling role cannot push another role's newest
17
+ # container past the retain window. That matters beyond disk hygiene: a
18
+ # container kamal-proxy has put to sleep is `exited`, so it is a removal
19
+ # candidate, and once it is gone every wake 404s. With `retain >= 1` a role's
20
+ # newest container always survives, and the slept one is always the newest —
21
+ # sleeping happens to the current release.
22
+ def app_containers(retain:, role:)
17
23
  pipe \
18
- docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
24
+ docker(:ps, "-q", "-a", *service_filter, *destination_filter, *role_filter(role), *stopped_containers_filters),
19
25
  "tail -n +#{retain + 1}",
20
26
  "while read container_id; do docker rm $container_id; done"
21
27
  end
@@ -35,4 +41,12 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
35
41
  def service_filter
36
42
  [ "--filter", "label=service=#{config.service}" ]
37
43
  end
44
+
45
+ def destination_filter
46
+ [ "--filter", "label=destination=#{config.destination}" ]
47
+ end
48
+
49
+ def role_filter(role)
50
+ [ "--filter", "label=role=#{role}" ]
51
+ end
38
52
  end
@@ -12,4 +12,9 @@ class Kamal::Commands::Server < Kamal::Commands::Base
12
12
  [ :ls, config.apps_directory ],
13
13
  [ :wc, "-l" ]
14
14
  end
15
+
16
+ # Lists TCP listeners on the given port, one line per listener, no header.
17
+ def listeners_on(port)
18
+ [ :ss, "-ltnH", :sport, "=", ":#{port}" ]
19
+ end
15
20
  end
@@ -127,12 +127,16 @@ class Kamal::Configuration::Accessory
127
127
  context: "accessories/#{name}/env"
128
128
  end
129
129
 
130
+ # The load balancer fans the app's own service out to role targets only
131
+ # (see Kamal::Cli::Proxy#loadbalancer), so an accessory is never behind it
132
+ # and must keep the host/TLS it registers with kamal-proxy directly.
130
133
  def initialize_proxy
131
134
  Kamal::Configuration::Proxy.new \
132
135
  config: config,
133
136
  proxy_config: accessory_config["proxy"],
134
137
  context: "accessories/#{name}/proxy",
135
- secrets: config.secrets
138
+ secrets: config.secrets,
139
+ load_balanced: false
136
140
  end
137
141
 
138
142
  def initialize_registry
@@ -256,14 +260,11 @@ class Kamal::Configuration::Accessory
256
260
  end
257
261
  end
258
262
 
263
+ # Ask each role for its own tagged hosts instead of re-parsing raw_config.servers here:
264
+ # a role may be written as a bare list or as a `hosts:` mapping, and Role already reads
265
+ # both. A second parser with a shape assumption dropped tagged hosts silently.
259
266
  def extract_hosts_from_config_with_tag(tag)
260
- if (servers_with_roles = config.raw_config.servers).is_a?(Hash)
261
- servers_with_roles.flat_map do |role, servers_in_role|
262
- servers_in_role.filter_map do |host|
263
- host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag)
264
- end
265
- end
266
- end
267
+ config.roles.flat_map { |role| role.hosts_with_tag(tag) }
267
268
  end
268
269
 
269
270
  def network
@@ -1,24 +1,36 @@
1
1
  class Kamal::Configuration::Boot
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- attr_reader :boot_config, :host_count
4
+ attr_reader :boot_config
5
5
 
6
- def initialize(config:)
7
- @boot_config = config.raw_config.boot || {}
8
- @host_count = config.all_hosts.count
9
- validate! boot_config
6
+ # Pass boot_config/context to scope this to a single role: the role's own `boot:` hash,
7
+ # and a context that points at the role in validation errors.
8
+ def initialize(config:, boot_config: nil, context: nil)
9
+ @boot_config = boot_config || config.raw_config.boot || {}
10
+ validate! @boot_config, context: context
10
11
  end
11
12
 
12
- def limit
13
+ # The group size to slice `hosts` by. A percentage only means something against the set
14
+ # it will actually be applied to, and that set is not known until run time — `--roles`
15
+ # and `--hosts` narrow it, and accessories are never booted at all. So callers pass the
16
+ # hosts they are about to group rather than Boot reaching for a host list of its own.
17
+ def limit_for(hosts)
13
18
  limit = boot_config["limit"]
14
19
 
15
20
  if limit.to_s.end_with?("%")
16
- [ host_count * limit.to_i / 100, 1 ].max
21
+ [ hosts.count * limit.to_i / 100, 1 ].max
17
22
  else
18
23
  limit
19
24
  end
20
25
  end
21
26
 
27
+ # Whether a limit is configured at all. Deliberately not a reader for the raw value:
28
+ # a percentage means nothing without the hosts it slices, so anything that needs the
29
+ # group size has to go through limit_for.
30
+ def limit?
31
+ boot_config["limit"].present?
32
+ end
33
+
22
34
  def wait
23
35
  boot_config["wait"]
24
36
  end
@@ -26,4 +38,23 @@ class Kamal::Configuration::Boot
26
38
  def parallel_roles
27
39
  boot_config["parallel_roles"]
28
40
  end
41
+
42
+ # SSHKit runner options expressing this pacing over `hosts`. Without a limit there is
43
+ # nothing to pace and `on` keeps its default parallel runner.
44
+ #
45
+ # `wait` is always explicit: SSHKit::Runner::Sequential and ::Group both fall back to a
46
+ # 2 second interval, where Kamal's own boot wait means "no sleep unless you asked for
47
+ # one". A limit of 1 goes through Sequential rather than Group, which runs one host per
48
+ # slice without spawning a thread to do it. Neither trails a wait past the last group —
49
+ # Sequential pops its final host, and Kamal patches Group to match (sshkit_with_ext.rb).
50
+ def runner_options_for(hosts)
51
+ limit = limit_for(hosts)
52
+ return {} unless limit
53
+
54
+ if limit == 1
55
+ { in: :sequence, wait: wait.to_i }
56
+ else
57
+ { in: :groups, limit: limit, wait: wait.to_i }
58
+ end
59
+ end
29
60
  end
@@ -155,6 +155,33 @@ accessories:
155
155
 
156
156
  # Proxy
157
157
  #
158
- # You can run your accessory behind the Kamal proxy. See kamal docs proxy for more information
158
+ # You can run your accessory behind the Kamal proxy for hostname-based
159
+ # routing and automatic SSL. The block accepts the same schema as the
160
+ # app-level `proxy:` (see `kamal docs proxy`), though root-only keys such as
161
+ # `loadbalancer` and `reboot_on_deploy` have no effect inside an accessory.
162
+ # Declaring a proxy here makes the accessory's hosts proxy hosts:
163
+ # `kamal proxy boot` (and `kamal deploy`) will run kamal-proxy on them
164
+ # automatically.
165
+ #
166
+ # Accessories are never load balanced. The loadbalancer fans the app's own
167
+ # service out to its role targets, so an accessory always registers its
168
+ # hostname and TLS with the kamal-proxy on its own host — whether or not the
169
+ # loadbalancer is active for the app. Point the accessory's DNS record at the
170
+ # accessory host, not at the loadbalancer.
171
+ #
172
+ # One caveat: accessory restarts are stop-then-start (no old/new container
173
+ # overlap), so expect a brief interruption when restarting or rebooting.
174
+ #
175
+ # For example:
176
+ #
177
+ # proxy:
178
+ # host: mysql-admin.example.com # Route this hostname to the accessory
179
+ # ssl: true # Terminate SSL with a Let's Encrypt cert
180
+ # app_port: 8080 # Container port the accessory listens on (default: 80)
181
+ # healthcheck:
182
+ # path: /health # Health check endpoint (default: /up)
183
+ #
184
+ # The `...` below is a placeholder: accessory proxy configuration accepts the
185
+ # full proxy schema and is validated against it when the accessory is loaded.
159
186
  proxy:
160
187
  ...
@@ -3,19 +3,34 @@
3
3
  # When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
4
4
  #
5
5
  # Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.
6
+ #
7
+ # This is a whole-deploy setting: `limit` slices the combined host list of every role, so it cannot
8
+ # hold one role back while another keeps booting. To pace a single role's hosts, set `boot` on that
9
+ # role instead (see kamal docs role).
6
10
 
7
11
  boot:
8
12
 
9
- # The number or percentage of hosts to boot at a time.
13
+ # The number or percentage of hosts to boot at a time, across all roles.
10
14
  # This can be an integer (e.g., 3) or a percentage string (e.g., 25%).
15
+ #
16
+ # A percentage counts the hosts this run is actually booting: app hosts only — accessories
17
+ # are not booted here — and narrowed by `--roles` / `--hosts`. So `25%` with `--roles web`
18
+ # is a quarter of web's hosts, not a quarter of the file.
11
19
  limit: 25%
12
20
 
13
21
  # The number of seconds to wait between booting each group of hosts.
22
+ #
23
+ # This fills the gap between one group and the next, so it needs `limit` to mean anything:
24
+ # without one, every host boots in a single group and there is no gap to fill. Kamal warns
25
+ # if you set `wait` on its own. Nothing waits after the final group either — the deploy
26
+ # moves on as soon as the last host is up.
14
27
  wait: 10
15
28
 
16
29
  # Whether to boot roles in parallel on a host.
17
30
  #
18
31
  # If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
19
32
  #
20
- # Defaults to false.
33
+ # Defaults to false. A role that sets its own `boot` needs this on, since per-role pacing has no
34
+ # meaning when each host walks its roles in turn — Kamal turns it on for you if you have not set
35
+ # it, and refuses the deploy if you set it to false.
21
36
  parallel_roles: true
@@ -136,7 +136,8 @@ allow_empty_roles: false
136
136
 
137
137
  # Retain containers
138
138
  #
139
- # How many old containers and images we retain, defaults to 5:
139
+ # How many old containers we retain per role, and how many images we retain,
140
+ # defaults to 5:
140
141
  retain_containers: 3
141
142
 
142
143
  # Minimum version