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
@@ -12,6 +12,36 @@
12
12
  #
13
13
  proxy:
14
14
 
15
+
16
+ # Where each option lives when a loadbalancer fronts the fleet
17
+ #
18
+ # Every deploy option has exactly one home (see
19
+ # Kamal::Configuration::Proxy::DEPLOY_OPTION_DISPOSITIONS - the layering
20
+ # contract that enforces this):
21
+ #
22
+ # edge - applied only by the loadbalancer, stripped from the per-host
23
+ # proxies: host/hosts, ssl (certificates, on-demand, mTLS),
24
+ # ssl_redirect, ssl_staging, ssl_domains, basic_auth,
25
+ # allow_ips, client_ip, rate_limit, session_affinity,
26
+ # canonical_host, redirects, cache, read_routing
27
+ #
28
+ # per-app - applied only by the per-host proxies, next to the app:
29
+ # headers, rewrites, intercept_errors, sleep, compress
30
+ #
31
+ # both - each layer runs its own copy, deliberately: healthcheck,
32
+ # response/request timeouts, path timeouts, target pool
33
+ # tuning, buffering, path_prefix, strip_path_prefix,
34
+ # forward_headers, logging, exclude_metrics_paths
35
+ #
36
+ # Without a loadbalancer the single proxy is every layer at once and the
37
+ # whole surface applies to it. `loadbalancer`, `reboot_on_deploy` and `run`
38
+ # configure the containers themselves rather than a deployment, so they
39
+ # have no layer.
40
+
41
+ # ==========================================================================
42
+ # 1. Essentials — enough for most apps
43
+ # ==========================================================================
44
+
15
45
  # Hosts
16
46
  #
17
47
  # The hosts that will be used to serve the app. The proxy will only route requests
@@ -22,17 +52,71 @@ proxy:
22
52
  #
23
53
  # Specify one of `host` or `hosts`.
24
54
  host: foo.example.com
55
+
25
56
  hosts:
26
57
  - foo.example.com
27
58
  - bar.example.com
28
-
59
+
29
60
  # Loadbalancer
30
61
  #
31
62
  # Specify a host to run the loadbalancer on. The loadbalancer will distribute requests
32
63
  # to all web hosts. If not specified but multiple web hosts are configured, the first
33
64
  # web host will be used as the loadbalancer host.
65
+ #
66
+ # The host may be a dedicated machine outside of `servers:`, or one of the web hosts —
67
+ # in which case the loadbalancer takes over that host's proxy container.
68
+ #
69
+ # Set `true` to always run the loadbalancer on the first host of the primary role, even
70
+ # when that role has a single host, or `false` to opt out of the automatic activation
71
+ # for multi-host primary roles.
34
72
  loadbalancer: lb.example.com
35
73
 
74
+ # Sharing one loadbalancer between several kamal apps
75
+ #
76
+ # More than one kamal app may point `loadbalancer:` at the same host. Each app
77
+ # registers its own service on the shared kamal-proxy and keeps its own
78
+ # deploy.yml; the load balancer multiplexes them by hostname.
79
+ #
80
+ # The rules kamal enforces for that topology:
81
+ #
82
+ # * Service state survives a reboot. kamal-proxy persists its services in the
83
+ # `kamal-loadbalancer-config` volume (or `kamal-proxy-config` when the
84
+ # loadbalancer shares a proxy host), and `kamal proxy reboot` only replaces
85
+ # the container. Rebooting from app A does not drop app B's routes — the
86
+ # surviving service list is printed after the restart.
87
+ #
88
+ # * Service names must be unique across apps. The loadbalancer registers a
89
+ # service under the bare `service:` name, so two apps — or two destinations
90
+ # of one app — sharing a name would take over each other's routes. The first
91
+ # app to deploy claims the name in `.kamal/loadbalancer/services/<service>`
92
+ # on the loadbalancer host; a second app deploying the same name fails at
93
+ # deploy time instead of silently winning.
94
+ #
95
+ # * Apps sharing a loadbalancer must agree on `proxy/run`. All of them boot the
96
+ # same container, so the first app to boot records its run configuration in
97
+ # `.kamal/loadbalancer/run_config`. Another app booting with a different
98
+ # `proxy/run` fails; the same app changing its own gets the usual drift
99
+ # warning and applies it on `kamal proxy reboot`.
100
+ #
101
+ # * `kamal proxy remove` refuses while other apps are installed on the
102
+ # loadbalancer host, exactly as it does for a proxy host. Use `--force` to
103
+ # override, which removes the shared loadbalancer for every app on it.
104
+
105
+ # Automatic proxy reboot on deploy
106
+ #
107
+ # When `kamal deploy` detects that the running kamal-proxy container was
108
+ # started with a different image, version or run options than the current
109
+ # configuration, it reboots the proxy automatically — one host at a time —
110
+ # before booting the app.
111
+ #
112
+ # Set to false to opt out. Kamal will then print a warning when drift is
113
+ # detected and leave the proxy untouched until you run `kamal proxy reboot`.
114
+ #
115
+ # Root-level `proxy` setting only; ignored inside role-specific proxy blocks.
116
+ #
117
+ # Defaults to true:
118
+ reboot_on_deploy: false
119
+
36
120
  # App port
37
121
  #
38
122
  # The port the application container is exposed on.
@@ -63,9 +147,29 @@ proxy:
63
147
  #
64
148
  # Kamal supports loading custom SSL certificates directly from secrets. You should
65
149
  # pass a hash mapping the `certificate_pem` and `private_key_pem` to the secret names.
150
+ #
151
+ # The hash is also home to the rest of the TLS surface:
152
+ #
153
+ # `on_demand_url` turns on on-demand TLS: instead of a fixed `host` list, the
154
+ # proxy asks this endpoint whether it may issue a certificate for the hostname
155
+ # in an incoming handshake. A path is resolved against a healthy app target; an
156
+ # absolute http(s) URL is called directly. Answer 2xx to approve. On-demand TLS
157
+ # replaces the static hostname list, so it cannot be combined with
158
+ # `host`/`hosts`, with `certificate_pem`, or with `ssl_domains` — kamal rejects
159
+ # those at config time, because kamal-proxy would reject the deploy and there
160
+ # is no sensible winner to pick.
161
+ #
162
+ # `client_ca_pem` requires mutual TLS: clients must present a certificate
163
+ # signed by this CA bundle. Like `certificate_pem` it names a secret in
164
+ # `.kamal/secrets`; the content is uploaded next to the app's TLS material
165
+ # under `.kamal/proxy/apps-config`, and the proxy is given the path it sees
166
+ # inside its own container. An empty secret fails the deploy rather than
167
+ # silently turning mTLS off.
66
168
  ssl:
67
169
  certificate_pem: CERTIFICATE_PEM
68
170
  private_key_pem: PRIVATE_KEY_PEM
171
+ on_demand_url: https://app.example.com/api/v1/tls/ask
172
+ client_ca_pem: CLIENT_CA_PEM
69
173
  # ### Notes
70
174
  # - If the certificate or key is missing or invalid, deployments will fail.
71
175
  # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in source control.
@@ -77,20 +181,37 @@ proxy:
77
181
  # HTTPS traffic), you can disable this redirect by setting `ssl_redirect: false`:
78
182
  ssl_redirect: false
79
183
 
80
- # Forward headers
81
- #
82
- # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
184
+ # SSL staging
83
185
  #
84
- # If you are behind a trusted proxy, you can set this to `true` to forward the headers.
186
+ # When automatic SSL is enabled, use the Let's Encrypt staging environment
187
+ # for certificate provisioning, so you can test your SSL configuration
188
+ # without running into Let's Encrypt's production rate limits. Certificates
189
+ # issued by the staging environment are not trusted by browsers.
85
190
  #
86
- # By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
87
- # will forward them if it is set to `false`.
88
- forward_headers: true
191
+ # Defaults to `false`:
192
+ ssl_staging: true
89
193
 
90
- # Response timeout
194
+ # Healthcheck
91
195
  #
92
- # How long to wait for requests to complete before timing out, defaults to 30 seconds:
93
- response_timeout: 10
196
+ # When deploying, the proxy will by default hit `/up` once every second until we hit
197
+ # the deploy timeout, with a 5-second timeout for each request.
198
+ #
199
+ # Once the app is up, the proxy will stop hitting the healthcheck endpoint.
200
+ #
201
+ # By default, the healthcheck is sent to the app port. Set `port` to check a
202
+ # different port on the container, and `host` to set the Host header sent with
203
+ # healthcheck requests.
204
+ healthcheck:
205
+ interval: 3
206
+ path: /health
207
+ timeout: 3
208
+ port: 3001
209
+ host: health.example.com
210
+
211
+
212
+ # ==========================================================================
213
+ # 2. Traffic & routing
214
+ # ==========================================================================
94
215
 
95
216
  # Path-based routing
96
217
  #
@@ -102,11 +223,13 @@ proxy:
102
223
  #
103
224
  # When using path_prefix you can supply multiple routes separated by commas.
104
225
  path_prefix: "/api,/oauth_callback"
226
+
105
227
  # You can also specify paths as a list of paths, the configuration will be
106
228
  # rolled together into a comma separated string.
107
229
  path_prefixes:
108
230
  - "/api"
109
231
  - "/oauth_callback"
232
+
110
233
  # By default, the path prefix will be stripped from the request before it is forwarded upstream.
111
234
  #
112
235
  # So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
@@ -115,16 +238,317 @@ proxy:
115
238
  # specify --strip-path-prefix=false
116
239
  strip_path_prefix: false
117
240
 
118
- # Healthcheck
241
+ # Forward headers
119
242
  #
120
- # When deploying, the proxy will by default hit `/up` once every second until we hit
121
- # the deploy timeout, with a 5-second timeout for each request.
243
+ # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
122
244
  #
123
- # Once the app is up, the proxy will stop hitting the healthcheck endpoint.
124
- healthcheck:
125
- interval: 3
126
- path: /health
127
- timeout: 3
245
+ # If you are behind a trusted proxy, you can set this to `true` to forward the headers.
246
+ #
247
+ # By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
248
+ # will forward them if it is set to `false`.
249
+ forward_headers: true
250
+
251
+ # Header rules
252
+ #
253
+ # Rewrite headers on their way to the app and on their way back out, without
254
+ # the app knowing. `set` replaces whatever was there, `add` appends and keeps
255
+ # it, `remove` strips it.
256
+ #
257
+ # Names are canonicalised, and values have their leading and trailing
258
+ # whitespace trimmed. A value may contain colons — a CSP naming a scheme or a
259
+ # port survives intact.
260
+ #
261
+ # Two things kamal rejects rather than letting them fail late: a value with a
262
+ # newline or carriage return in it (that is response splitting, and shell
263
+ # escaping would silently turn it into a literal backslash-n), and a *request*
264
+ # rule naming `Host` — Go carries the host outside the header map, so the rule
265
+ # would do nothing at all.
266
+ #
267
+ # Response rules apply to what the app returned. Error pages, redirects, and
268
+ # auth or rate-limit rejections come from the proxy itself and are unaffected.
269
+ headers:
270
+ request:
271
+ set:
272
+ X-Forwarded-Host: app.example.com
273
+ add:
274
+ X-Request-Source: kamal
275
+ remove:
276
+ - X-Internal-Token
277
+ response:
278
+ set:
279
+ Strict-Transport-Security: max-age=31536000
280
+ add:
281
+ X-Served-By: kamal-proxy
282
+ remove:
283
+ - Server
284
+
285
+ # Redirects and rewrites
286
+ #
287
+ # A redirect answers the client with a `Location`; a rewrite changes the path
288
+ # the app receives while the client's URL stays as it was — which is what an
289
+ # SPA serving its own routes out of `/index.html` needs.
290
+ #
291
+ # `from` is a regular expression **anchored to the whole path**, so `/old` does
292
+ # not fire on `/not-old-either`. `to` is a path on this host, and for a
293
+ # redirect may also be a full http(s) URL. Captures are available as `$1`,
294
+ # `$2`, … Rules are tried in order and the first match wins.
295
+ #
296
+ # ### Watch the path prefix
297
+ #
298
+ # Both match the path **the client asked for, before `path_prefix` stripping**.
299
+ # An app mounted at `path_prefix: /api` with `strip_path_prefix` on sees
300
+ # `/users`, but a rule here still has to be written against `/api/users`. This
301
+ # is the part that is easy to get wrong.
302
+ #
303
+ # `status` applies to redirects only and must be 301, 302, 303, 307 or 308.
304
+ # It defaults to 301, so say 302 explicitly for anything you may want back.
305
+ redirects:
306
+ - from: /old
307
+ to: /new
308
+ - from: /gone/(.*)
309
+ to: https://elsewhere.example.com/$1
310
+ status: 302
311
+
312
+ rewrites:
313
+ - from: /api/(.*)
314
+ to: /v2/$1
315
+
316
+ # Canonical host
317
+ #
318
+ # Redirect every request to this host, to force apex or www one way.
319
+ canonical_host: www.example.com
320
+
321
+ # Intercept error statuses
322
+ #
323
+ # Replace these statuses coming from the app with the proxy's own error pages,
324
+ # discarding whatever body the app sent. 4xx and 5xx codes only.
325
+ #
326
+ # This pairs with the root-level `error_pages_path`. It works without it, but
327
+ # not usefully: with no pages to render, the proxy falls back to a bare
328
+ # plaintext status line, so the app's own error page is thrown away and
329
+ # replaced by the words "Bad Gateway". Kamal warns when you do that.
330
+ intercept_errors:
331
+ - 502
332
+ - 503
333
+ #
334
+ # ### Note
335
+ # Everything in this section applies on the per-host proxy, including when a
336
+ # loadbalancer is configured — unlike TLS and access control, which move to the
337
+ # loadbalancer. Applying them at both layers would append an `add` header twice
338
+ # and run a rewrite over its own output.
339
+
340
+
341
+ # ==========================================================================
342
+ # 3. Security & access
343
+ # ==========================================================================
344
+
345
+ # Basic auth
346
+ #
347
+ # Require HTTP Basic credentials on every request to this service. Requests
348
+ # without valid credentials get a 401 with a `WWW-Authenticate` challenge.
349
+ # The health check path stays open, so deploys are unaffected.
350
+ #
351
+ # Set exactly one of `password_secret` or `password` — never both.
352
+ #
353
+ # Prefer `password_secret`: it names an entry in `.kamal/secrets`, so the
354
+ # password never lives in this file.
355
+ #
356
+ # basic_auth:
357
+ # username: admin
358
+ # password_secret: WEB_BASIC_AUTH_PASSWORD
359
+ #
360
+ # `password` sets the value directly. If you use it, interpolate rather than
361
+ # committing a literal:
362
+ #
363
+ # basic_auth:
364
+ # username: admin
365
+ # password: <%= ENV["WEB_BASIC_AUTH_PASSWORD"] %>
366
+ #
367
+ basic_auth:
368
+ username: admin
369
+ password: <%= ENV["WEB_BASIC_AUTH_PASSWORD"] %>
370
+ password_secret: WEB_BASIC_AUTH_PASSWORD
371
+ #
372
+ # ### Notes
373
+ # - Requires a kamal-proxy that supports `--basic-auth`. This is newer than
374
+ # the current `MINIMUM_VERSION`, so make sure your proxy is up to date
375
+ # before enabling it — an older proxy fails the deploy on an unknown flag.
376
+ # - Basic credentials are replayable and are sent on every request. Use this
377
+ # with `ssl: true`, or terminate TLS in front of the proxy.
378
+ # - kamal-proxy removes the `Authorization` header before forwarding, so a
379
+ # service behind basic auth cannot also pass credentials through to its
380
+ # target. When load balancing, the credentials are enforced by the load
381
+ # balancer only.
382
+
383
+ # Dynamic TLS domains
384
+ #
385
+ # kamal-proxy can learn TLS hostnames from your application at runtime
386
+ # instead of fixing them at deploy time. It polls `source` — a path
387
+ # (resolved against a healthy app target) or an absolute http(s) URL —
388
+ # for the domain list and manages Let's Encrypt certificates for it
389
+ # automatically. When `source` is set, `ssl: true` is allowed without
390
+ # `host`/`hosts`.
391
+ #
392
+ # `interval` is the poll interval in seconds (proxy default: 300).
393
+ #
394
+ # `batch_size` controls how many domains share a certificate: 1 (the
395
+ # proxy default) issues per-domain certs; 2-25 enables stable SAN batching.
396
+ #
397
+ # Authentication tokens for the poll endpoint and the refresh nudge are
398
+ # read by the proxy from the `KAMAL_PROXY_DOMAINS_TOKEN` and
399
+ # `KAMAL_PROXY_REFRESH_TOKEN` environment variables. Set them on the proxy
400
+ # container via `proxy.run.options.env` — never as deploy flags, which
401
+ # leak into process listings and audit logs.
402
+ ssl_domains:
403
+ source: /api/v1/kamal/domains
404
+ interval: 300
405
+ batch_size: 1
406
+
407
+ # Who the client is
408
+ #
409
+ # Rate limiting and IP allow lists are both only as correct as the address they
410
+ # key on, so configure this first if anything sits in front of kamal-proxy.
411
+ #
412
+ # With no `trusted_proxies`, the client is always the address that opened the
413
+ # connection — nothing a client sends can influence it, which is what makes the
414
+ # allow list meaningful.
415
+ #
416
+ # Once you declare `trusted_proxies`, and only when the connecting address is
417
+ # one of them, kamal-proxy reads the forwarded chain instead: it walks the
418
+ # chain from the nearest hop backwards past every proxy you declared, and the
419
+ # first address none of your proxies wrote is the client. **List every hop**,
420
+ # not only the one that connects to kamal-proxy — a chain it cannot resolve
421
+ # denies the request rather than falling back to the connecting address.
422
+ #
423
+ # `header` names the header carrying the original client IP (`CF-Connecting-IP`
424
+ # behind Cloudflare, `True-Client-IP` behind some others); kamal-proxy reads it
425
+ # instead of `X-Forwarded-For`. It is only honoured when `trusted_proxies` is
426
+ # set, because otherwise it is just something the client wrote — kamal rejects
427
+ # that combination rather than appearing to honour it.
428
+ #
429
+ # Addresses and ranges are plain IPv4/IPv6 or CIDR. Write IPv4 as IPv4, not as
430
+ # an IPv4-mapped IPv6 range, and leave IPv6 zones off — neither matches
431
+ # anything. If clients reach you over IPv6, list IPv6 ranges too, or they are
432
+ # denied.
433
+ client_ip:
434
+ header: CF-Connecting-IP
435
+ trusted_proxies:
436
+ - 173.245.48.0/20
437
+ - 2400:cb00::/32
438
+
439
+ # Rate limiting
440
+ #
441
+ # A per-client token bucket. Requests over the limit get a 429. IPv6 clients
442
+ # are counted per /64, since one client can pick any address inside its own.
443
+ #
444
+ # `requests` is requests per second and may be fractional — 0.5 is one request
445
+ # every two seconds. `burst` is how many requests a client may make back to
446
+ # back before the limit applies (default: the rate, rounded up). `exempt` lists
447
+ # addresses and ranges the limit skips, for monitoring and health probes.
448
+ rate_limit:
449
+ requests: 100
450
+ burst: 20
451
+ exempt:
452
+ - 10.0.0.0/8
453
+
454
+ # IP allow list
455
+ #
456
+ # Serve this service only to these addresses and ranges; everything else gets a
457
+ # 403. Combine with `client_ip` above when you are behind a CDN, or the list is
458
+ # matched against the CDN's addresses rather than your visitors'.
459
+ allow_ips:
460
+ - 10.0.0.0/8
461
+ - 192.168.0.0/16
462
+ #
463
+ # ### Notes
464
+ # - The health check path is served without an address check and without a rate
465
+ # limit, so it stays reachable during a deploy. That means it cannot be `/` —
466
+ # kamal rejects `healthcheck: path: /` while either feature is on, because it
467
+ # would leave the whole service open. The default `/up` is fine.
468
+ # - When a loadbalancer is configured, all of this moves to the loadbalancer:
469
+ # the per-host proxies see the loadbalancer as their peer, so an allow list
470
+ # there would refuse every request and one rate limiter would count the whole
471
+ # fleet as a single client.
472
+
473
+
474
+ # ==========================================================================
475
+ # 4. Performance & observability
476
+ # ==========================================================================
477
+
478
+ # Two different deadlines
479
+ #
480
+ # `response_timeout` bounds how long the app may take to *start* answering —
481
+ # its clock stops once response headers arrive. `request_timeout` bounds the
482
+ # whole request, including streaming the body back to the client.
483
+ #
484
+ # They are not interchangeable, and the usual confusion is setting the first
485
+ # and still seeing requests hang: a slow trickle of body bytes never trips
486
+ # `response_timeout`, because the app answered promptly. WebSocket and
487
+ # event-stream responses are exempt from `request_timeout`.
488
+ #
489
+ # `response_timeout` defaults to 30 seconds; `request_timeout` defaults to 0,
490
+ # meaning no limit.
491
+ response_timeout: 10
492
+
493
+ request_timeout: 30
494
+
495
+ # Per-path timeouts
496
+ #
497
+ # Override either deadline below a path prefix. Values are Go duration strings
498
+ # ("90s", "5m") or plain seconds; 0 removes the limit for that prefix, which
499
+ # suits streaming and SSE endpoints.
500
+ path_response_timeouts:
501
+ "/api/reports": "5m"
502
+ "/stream": 0
503
+
504
+ path_request_timeouts:
505
+ "/uploads": "10m"
506
+ "/stream": 0
507
+
508
+ # Target connections and retries
509
+ #
510
+ # The connection pool between the proxy and this app's containers, and how
511
+ # hard the proxy tries to place a request on a healthy one.
512
+ #
513
+ # `max_conns` caps simultaneous connections per target, idle ones included.
514
+ # Requests over the cap queue rather than failing, and a streaming response
515
+ # holds its connection until the body closes — so pair a low cap with
516
+ # `request_timeout`. `max_idle_conns` caps the idle connections kept open.
517
+ # Both apply *per pool*, and every entry in `path_response_timeouts` adds another pool.
518
+ #
519
+ # `idle_conn_timeout` is how long an idle connection is kept; set it below the
520
+ # app's own keep-alive timeout, or the proxy will hand a request to a socket
521
+ # the app has already closed. `dial_timeout` bounds establishing a connection
522
+ # — `response_timeout` cannot cover that, since its clock starts only once the
523
+ # request has been written.
524
+ #
525
+ # `try_duration` keeps looking for a healthy target for that long before
526
+ # giving up, with `try_interval` between attempts. Only idempotent requests
527
+ # without a body are re-sent to another target.
528
+ #
529
+ # ### What 0 means here
530
+ #
531
+ # Not the same thing for every key, because the proxy resolves its own
532
+ # defaults from a zero:
533
+ #
534
+ # * `max_conns: 0` — unlimited
535
+ # * `try_duration: 0` — a single attempt, no retrying
536
+ # * `request_timeout: 0` — no limit
537
+ # * `max_idle_conns: 0` — the proxy's default of 100, **not** "keep none"
538
+ # * `idle_conn_timeout: 0` — the proxy's default of 90s
539
+ # * `dial_timeout: 0` — the proxy's default of 30s
540
+ # * `try_interval: 0` — the proxy's default of 250ms
541
+ #
542
+ # So there is no way to ask for zero idle connections; leave the key unset
543
+ # unless you mean to change it.
544
+ target:
545
+ max_conns: 100
546
+ max_idle_conns: 10
547
+ idle_conn_timeout: 90
548
+ dial_timeout: 5
549
+ disable_keep_alives: false
550
+ try_duration: 30
551
+ try_interval: 1
128
552
 
129
553
  # Buffering
130
554
  #
@@ -142,6 +566,113 @@ proxy:
142
566
  max_response_body: 0
143
567
  memory: 2_000_000
144
568
 
569
+ # Response compression
570
+ #
571
+ # Serve gzip, brotli or zstd without the app knowing about it. Responses the
572
+ # app already encoded, event streams, and media types that do not shrink are
573
+ # passed through untouched.
574
+ #
575
+ # `compress: true` is the common case: it offers zstd, br and gzip in that
576
+ # order — best ratio first, and the client's own `Accept-Encoding` preference
577
+ # still wins — and leaves the length and media-type defaults to the proxy.
578
+ #
579
+ # compress: true
580
+ #
581
+ # The block form is for when you want something narrower. `encodings` names
582
+ # what to offer, most preferred first, from `gzip`, `br` (or `brotli`) and
583
+ # `zstd`; naming it is enough to switch compression on. `min_length` is the
584
+ # response size in bytes below which compressing costs more than it saves
585
+ # (proxy default 1024; set 1 to compress everything).
586
+ #
587
+ # An explicit `enabled: false` switches compression off even when `encodings`
588
+ # are named — it is the off switch for a block whose tuning you want to keep.
589
+ #
590
+ # `content_types` **replaces** the proxy's built-in list of compressible media
591
+ # types rather than adding to it, so name every type you want compressed, not
592
+ # just the extra ones. Entries are exact types or type wildcards. Note that
593
+ # `text/event-stream` is never compressed unless you name it here — holding
594
+ # bytes back to fill the encoder's window is exactly what an event stream's
595
+ # client asked you not to do.
596
+ compress:
597
+ enabled: true
598
+ encodings:
599
+ - zstd
600
+ - br
601
+ - gzip
602
+ content_types:
603
+ - text/html
604
+ - application/json
605
+ min_length: 1024
606
+
607
+ # Response cache
608
+ #
609
+ # An RFC 9111 shared cache in front of this service. Nothing is stored unless
610
+ # the app marks a response `public` with an `s-maxage` or `max-age`, and a
611
+ # response carrying `Set-Cookie` is refused unless `allow_set_cookie` says
612
+ # otherwise — a shared cache replaying one client's cookie to the next is the
613
+ # worst thing it could do.
614
+ #
615
+ # This block is the *policy*, and it is per service. Where the entries live is
616
+ # proxy-wide and set under `run/cache` below.
617
+ #
618
+ # Only `enabled` is required. Everything else keeps kamal-proxy's own default
619
+ # until you set it.
620
+ #
621
+ # `max_ttl` caps the lifetime the app asks for, in seconds, so one mistaken
622
+ # directive cannot pin content until the next deploy. `max_body` is the largest
623
+ # response body to store, in bytes — the same plain byte counts `buffering`
624
+ # uses. Bigger responses still reach the client, they are just not kept.
625
+ #
626
+ # `max_variants` is how many representations one URL may hold when the app
627
+ # negotiates with `Vary` (negative switches automatic variants off and refuses
628
+ # a varying response outright).
629
+ #
630
+ # `vary_headers` and `vary_cookies` add request headers and cookie names to the
631
+ # cache key for EVERY response this service stores. Headers the app already
632
+ # names in `Vary` are keyed automatically per URL and need no entry here —
633
+ # naming one moves it into the key for every path in the service, which is
634
+ # usually not what you want.
635
+ #
636
+ # ### Administering it
637
+ #
638
+ # `kamal proxy cache stats` reports what the cache is holding (add `--count`
639
+ # to measure entries and bytes per service, `--json` for the raw report),
640
+ # and `kamal proxy cache purge` drops this app's cached responses
641
+ # (`--path-prefix /assets` to narrow it). Both run on the layer that owns
642
+ # the cache - the loadbalancer when load balancing, else each proxy host.
643
+ #
644
+ # ### When it is not caching
645
+ #
646
+ # A cache that quietly stores nothing is the usual first surprise. Start with
647
+ # `kamal proxy cache stats`; kamal-proxy also explains every refusal — check
648
+ # `kamal proxy logs` for the reason, and the `cache_refusals_total` metric
649
+ # (by `reason`) if you run with `metrics_port`.
650
+ # The common reasons are a missing `Cache-Control: public, max-age=...` on the
651
+ # app's response, a `Set-Cookie` header, a body over `max_body`, and
652
+ # `variant_limit` from `max_variants`.
653
+ cache:
654
+ enabled: true
655
+ max_ttl: 300
656
+ max_body: 1_048_576
657
+ max_variants: 8
658
+ vary_headers:
659
+ - Accept-Encoding
660
+ - Accept-Language
661
+ vary_cookies:
662
+ - locale
663
+ allow_set_cookie: false
664
+
665
+ # Paths to leave out of the Prometheus metrics
666
+ #
667
+ # Request paths that should not be counted, typically health and readiness
668
+ # endpoints that would otherwise dominate the histograms.
669
+ #
670
+ # This is a per-service deploy setting even though it reads like metrics
671
+ # configuration — where the metrics are served and who may read them are
672
+ # proxy-wide and live under `run/metrics_port` and `run/metrics_allow_ips`.
673
+ exclude_metrics_paths:
674
+ - /up
675
+
145
676
  # Logging
146
677
  #
147
678
  # Configure request logging for the proxy.
@@ -155,6 +686,88 @@ proxy:
155
686
  - X-Request-ID
156
687
  - X-Request-Start
157
688
 
689
+
690
+ # ==========================================================================
691
+ # 5. Fleet — multi-host behaviour
692
+ # ==========================================================================
693
+
694
+ # Read-only targets
695
+ #
696
+ # kamal-proxy can split traffic between the deployed (writer) targets and a
697
+ # set of read-only targets, e.g. app instances backed by database replicas.
698
+ # Read requests are routed to the read targets; write requests always go to
699
+ # the writers.
700
+ #
701
+ # Targets are host:port addresses reachable from the proxy.
702
+ #
703
+ # `websockets` routes WebSocket traffic to the read targets too (default
704
+ # `false`). `writer_affinity_timeout` is how long, in seconds, a client's
705
+ # reads stick to the writer after it makes a write, so clients always read
706
+ # their own writes (default 1 second).
707
+ #
708
+ # When a loadbalancer fronts the fleet, read routing is decided there — at
709
+ # the only layer that sees the whole fleet.
710
+ read_routing:
711
+ targets:
712
+ - 192.168.0.2:3000
713
+ - 192.168.0.3:3000
714
+ websockets: true
715
+ writer_affinity_timeout: 10
716
+
717
+ # Session affinity
718
+ #
719
+ # Keep each client on the target that first served it, for apps holding session
720
+ # state in the instance. Off by default, and rightly so — every request is
721
+ # otherwise free to go to whichever target is best placed to serve it.
722
+ #
723
+ # The client carries an opaque HttpOnly cookie naming its target. When that
724
+ # target leaves the pool the next request falls through to another one and is
725
+ # re-pinned, so a deploy does not strand anybody. Reads served by a
726
+ # `read_targets` replica are never pinned.
727
+ #
728
+ # `cookie` renames the pin cookie; kamal-proxy picks a sensible default.
729
+ session_affinity:
730
+ enabled: true
731
+ cookie: _kamal_affinity
732
+
733
+ # Scale to zero
734
+ #
735
+ # Stop this service's containers after `after` seconds with no traffic, and
736
+ # start them again on the next request, which is held until they are healthy.
737
+ # Health checks and the proxy's own TLS probes are not traffic and never wake a
738
+ # sleeping service.
739
+ #
740
+ # `wake_timeout` is how long a request waits for the containers to come back
741
+ # before giving up with a 503. `containers` names the containers to stop and
742
+ # start, replacing what the proxy infers from the target address — needed when
743
+ # a target names a network alias rather than a container.
744
+ #
745
+ # ### This needs the container runtime socket
746
+ #
747
+ # Stopping and starting containers means talking to the runtime, so the proxy
748
+ # must have been booted with `run/docker_socket` set. That is a boot-time
749
+ # prerequisite for a deploy-time setting, so kamal checks it while reading this
750
+ # file rather than letting the first request hang.
751
+ #
752
+ # Setting `run/docker_socket` also mounts that socket into the proxy container
753
+ # — the flag alone only says where to look. **Reaching the container runtime
754
+ # socket is root-equivalent on the host**, which is why it is a separate,
755
+ # explicit setting and not something enabling sleep does for you.
756
+ #
757
+ # Not compatible with `tls/on_demand_url`: a sleeping target cannot answer the
758
+ # ask endpoint, and waking one would let any hostname on the internet start a
759
+ # container.
760
+ sleep:
761
+ after: 300
762
+ wake_timeout: 30
763
+ containers:
764
+ - app-web
765
+
766
+
767
+ # ==========================================================================
768
+ # 6. Proxy container (run)
769
+ # ==========================================================================
770
+
158
771
  # Run configuration
159
772
  #
160
773
  # These options are used when booting the proxy container.
@@ -168,11 +781,218 @@ proxy:
168
781
  publish: false # Publish ports to the host (default: true)
169
782
  bind_ips: # List of IPs to bind to when publishing ports
170
783
  - 0.0.0.0
171
- registry: registry:4443 # Container registry for the kamal-proxy image
172
- # (defaults to Docker Hub)
173
- repository: myrepo/kamal-proxy # Container repository for the kamal-proxy image
174
- # (defaults to `ghcr.io/mhenrixon/kamal-proxy`)
175
- version: v0.8.0 # Version tag of the kamal-proxy image to use
784
+ registry: registry:4443 # Extra registry prefix for the kamal-proxy image
785
+ # (default: none). If you set this, also override
786
+ # `repository` to a host-less path (e.g.
787
+ # myfork/kamal-proxy) - the default repository
788
+ # below already embeds its ghcr.io host
789
+ repository: ghcr.io/mhenrixon/kamal-proxy # Container repository for the
790
+ # kamal-proxy image (this is the default)
791
+ version: v1.0.0.1 # Version tag of the kamal-proxy image to use.
792
+ # Defaults to the minimum version this gem
793
+ # requires - only pin it to roll forward early,
794
+ # never below the default
795
+ port_holder: true # Zero-downtime proxy reboots (default: false).
796
+ # Runs a minimal long-lived kamal-proxy-net container that
797
+ # owns the published ports; proxy generations join its
798
+ # network namespace and overlap during a reboot, so config
799
+ # and version changes apply without dropping requests.
800
+ # Adopting (or leaving) this mode takes one final
801
+ # reboot with a brief gap.
802
+ # ACME / Let's Encrypt
803
+ #
804
+ # Certificate issuance for the whole proxy, including DNS-01 challenges.
805
+ # These are proxy-wide, not per-app: every service on this proxy shares the
806
+ # ACME account and the DNS provider.
807
+ #
808
+ # `dns_provider` names the provider that answers DNS-01 challenges. Leave it
809
+ # at `auto` to let the proxy pick from the credentials it can see. An
810
+ # unsupported name is rejected here, at config time - kamal-proxy would only
811
+ # log a warning and then never issue a certificate.
812
+ #
813
+ # kamal-proxy also accepts short aliases for the canonical names - `cf`
814
+ # (cloudflare), `do` (digitalocean), `gcp`/`google`/`googledns` (gcloud),
815
+ # `gd` (godaddy), `hz` (hetzner), `nc` (namecheap), `aws`/`r53` (route53)
816
+ # and `vr` (vultr). Prefer the canonical name; the aliases exist so a
817
+ # config written for the proxy's own CLI is not rejected here.
818
+ #
819
+ # `prefer_wildcard` asks for a wildcard certificate when the DNS provider
820
+ # supports one, and `http_fallback` falls back to an HTTP-01 challenge when
821
+ # DNS-01 fails. Both default to true in the proxy.
822
+ #
823
+ # `directory` overrides the ACME directory URL - point it at Let's Encrypt's
824
+ # staging environment while you are working out a DNS setup, so you do not
825
+ # burn production rate limits on failed attempts.
826
+ #
827
+ # `credentials` names entries in `.kamal/secrets` to pass to the proxy
828
+ # container as environment variables - the API credentials your DNS provider
829
+ # needs (`CF_API_TOKEN`, `LOOPIA_API_USER`, ...). Name them exactly as the
830
+ # provider expects them, since the name is what reaches the container.
831
+ #
832
+ # They are written to a 0600 env file on the proxy host and passed with
833
+ # `--env-file`, never on the command line: a DNS API token can rewrite your
834
+ # zone, and `docker run --env` would leave it in process listings and audit
835
+ # logs. Adding or renaming one changes the proxy's config digest, so the next
836
+ # deploy reboots the proxy to pick it up; rotating a value does not, so run
837
+ # `kamal proxy reboot` yourself after a rotation.
838
+ acme:
839
+ email: admin@example.com
840
+ dns_provider: cloudflare
841
+ prefer_wildcard: true
842
+ http_fallback: false
843
+ directory: https://acme-staging-v02.api.letsencrypt.org/directory
844
+ credentials:
845
+ - CF_API_TOKEN
846
+ # Logging and tracing
847
+ #
848
+ # `log_format` is the shape of every line the proxy writes, the access log
849
+ # included: `json` (the default) or `text`. `logfmt` is accepted as a name
850
+ # for `text`.
851
+ #
852
+ # `trace_context` decides what happens to the W3C `traceparent` header:
853
+ # `off` ignores it, `propagate` (the default) logs the trace an incoming
854
+ # request carries and forwards the header untouched, and `generate` also
855
+ # starts a trace when a request arrives without one.
856
+ log_format: json
857
+ trace_context: propagate
858
+
859
+ # TLS floor
860
+ #
861
+ # The lowest TLS version the HTTPS listener will negotiate: `1.2` (the
862
+ # default) or `1.3`. Quote it — unquoted YAML reads it as a number.
863
+ #
864
+ # This narrows what the listener accepts and cannot widen it. TLS 1.0 and
865
+ # 1.1 are refused outright, and HTTP/3 is always 1.3 regardless.
866
+ min_tls: "1.2"
867
+
868
+ # HTTP/3
869
+ #
870
+ # Offer HTTP/3 (QUIC) alongside HTTP/1.1 and HTTP/2. Needs UDP on the HTTPS
871
+ # port reachable as well as TCP.
872
+ #
873
+ # Defaults to false:
874
+ http3: true
875
+
876
+ # PROXY protocol
877
+ #
878
+ # Accept PROXY protocol v1/v2 headers on the HTTP and HTTPS listeners, so
879
+ # client addresses survive an L4 load balancer that cannot set
880
+ # `X-Forwarded-For`.
881
+ #
882
+ # **Set `proxy_protocol_allow_ips` with it.** Left empty, the proxy honours a
883
+ # PROXY header from any peer that can reach the port — and that header
884
+ # rewrites the connecting address every other feature keys on, including
885
+ # `allow_ips` and `rate_limit`. Kamal warns when you enable one without the
886
+ # other.
887
+ proxy_protocol: true
888
+ proxy_protocol_allow_ips:
889
+ - 10.0.0.0/8
890
+
891
+ # Metrics access
892
+ #
893
+ # Serve the metrics endpoint (see `metrics_port` above) only to these
894
+ # addresses or CIDR ranges. Empty means everyone who can reach the port.
895
+ #
896
+ # The metrics listener has no service behind it and matches the connecting
897
+ # address only — there is no forwarded chain to trust here, so
898
+ # `proxy/client_ip` does not apply.
899
+ #
900
+ # To leave paths out of the metrics themselves, see
901
+ # `proxy/exclude_metrics_paths`, which is a per-service deploy setting.
902
+ metrics_allow_ips:
903
+ - 10.0.0.0/8
904
+
905
+ # Server timeouts
906
+ #
907
+ # Connection-level deadlines for the proxy's own listeners, in seconds.
908
+ # These are proxy-wide; the per-service deadlines are `response_timeout` and
909
+ # `request_timeout` above.
910
+ #
911
+ # `read_header_timeout` bounds how long a client may take to send request
912
+ # headers. `read_timeout` bounds reading a whole request including its body,
913
+ # so a non-zero value truncates slow uploads. `write_timeout` bounds writing
914
+ # a whole response, so a non-zero value truncates SSE and streaming.
915
+ # `idle_timeout` is how long an idle keep-alive connection is kept.
916
+ #
917
+ # Zero disables each of them, and is a real value rather than "unset".
918
+ #
919
+ # `shutdown_timeout` is how long in-flight requests get to drain when the
920
+ # proxy stops.
921
+ read_header_timeout: 10
922
+ read_timeout: 30
923
+ write_timeout: 30
924
+ idle_timeout: 60
925
+ shutdown_timeout: 30
926
+
927
+ # Boot with an empty routing state when restoring the saved state fails,
928
+ # rather than refusing to start.
929
+ #
930
+ # Defaults to false:
931
+ ignore_restore_errors: true
932
+
933
+ # Bind listeners with SO_REUSEPORT, so an overlapping proxy generation can
934
+ # share the ports during a handoff.
935
+ #
936
+ # Defaults to false:
937
+ reuse_port: true
938
+
939
+ # Escape hatch for `kamal-proxy run`
940
+ #
941
+ # Anything kamal-proxy accepts that has no key above. Note the difference
942
+ # from `options` below, which many people expect to do this: `flags` goes to
943
+ # `kamal-proxy run`, `options` goes to `docker run`.
944
+ #
945
+ # `true` renders a bare flag; anything else renders `--flag value`. No
946
+ # translation is applied, so write Go durations and lists the way
947
+ # kamal-proxy wants them.
948
+ #
949
+ # A key here that a named setting already emits is rejected rather than
950
+ # passed twice — set one or the other.
951
+ flags:
952
+ some-new-flag: value
953
+
954
+ # Container runtime socket
955
+ #
956
+ # Path to the container runtime socket, which is what `proxy/sleep` needs in
957
+ # order to stop and start containers. Setting it does two things: it passes
958
+ # the path to kamal-proxy, and it mounts that path into the proxy container,
959
+ # since the flag alone only says where to look.
960
+ #
961
+ # **Reaching this socket is root-equivalent on the host.** It is a separate,
962
+ # explicit setting for exactly that reason — nothing else in deploy.yml turns
963
+ # it on for you.
964
+ #
965
+ # Defaults to none, with scale-to-zero disabled:
966
+ docker_socket: /var/run/docker.sock
967
+
968
+ # Response cache storage
969
+ #
970
+ # Where the entries cached by `proxy/cache` are kept. Proxy-wide: one store
971
+ # serves every service on this proxy, so unlike the policy above it belongs
972
+ # here, in the run configuration. Adding or removing it reboots the proxy
973
+ # on the next deploy.
974
+ #
975
+ # `store` is `memory` (the default — a per-node cache) or a `redis://` /
976
+ # `rediss://` URL that every proxy pointed at it shares, so one fetch warms
977
+ # the whole fleet.
978
+ #
979
+ # The URL never appears on the proxy's command line: kamal delivers it as
980
+ # `CACHE_STORE` in a 0600 env file on the host (alongside any ACME
981
+ # credentials), so a password in the URL stays out of process listings and
982
+ # kamal's audit log. The trade-off, same as the ACME credentials: changing
983
+ # only the URL's *value* does not move the drift digest — run
984
+ # `kamal proxy reboot` after rotating it.
985
+ #
986
+ # `store_timeout` only matters with a shared store, in seconds: how long
987
+ # the store may take to answer before the request goes to the app instead —
988
+ # a store that is slow or down then costs a cache, never a failed request.
989
+ cache:
990
+ store: redis://cache.example.com:6379/0
991
+ store_timeout: 2
992
+ memory_size: 134_217_728
993
+ # `options` are `docker run` options for the proxy container - resource
994
+ # limits, labels, extra mounts - NOT kamal-proxy flags. A kamal-proxy flag
995
+ # the gem has no key for goes in `flags` above instead.
176
996
  options: # Additional options to pass to `docker run`
177
997
  label:
178
998
  - custom.label=kamal-proxy