Skip to content

feat(proxy): expose the response cache in deploy.yml - #86

Merged
mhenrixon merged 1 commit into
dashfrom
issue-75-response-cache
Jul 30, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
issue-75-response-cache

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

The RFC 9111 response cache, reachable from deploy.yml. Policy per service, store proxy-wide.

proxy:
  cache:
    enabled: true
    max_ttl: 300
    max_body: 1_048_576
    max_variants: 8
    vary_headers: [ Accept-Encoding ]
    vary_cookies: [ locale ]
    allow_set_cookie: false
  run:
    cache:
      store: redis://cache.example.com:6379/0
      store_timeout: 2
      memory_size: 134_217_728
      lease_ttl: 30
      lease_wait: 1
  • lib/kamal/configuration/proxy.rb — a cache_options group merged into deploy_options, alongside tls_options and tls_domains_options.
  • lib/kamal/configuration/proxy/run.rb — a cache_options group merged into run_command_options, so the store rides config_digest.
  • lib/kamal/configuration/validator/proxy.rb — two silent failures become config errors (below).
  • lib/kamal/utils.rb — seconds_duration lifted out of Proxy's privates so Proxy::Run can use it too.
  • test/proxy_flag_coverage_test.rb — both issue-75 waivers deleted; all twelve flags proven emitted from the maximal fixture.

Closes #75

Two silent failures turned into config errors

Config kamal-proxy's behaviour Now
cache: with max_ttl etc. but no enabled: true ignores the policy wholesale — a cache that never caches proxy/cache: max_ttl has no effect without enabled: true
run.cache.store: memcached://… preRun rejects it, so the proxy container exits at boot proxy/run/cache: store must be 'memory' or a redis:// or rediss:// URL

The second matters more than it looks: an unsupported store does not fail a deploy, it takes the proxy down.

Test plan

  • bundle exec rubocop --parallel — clean, 210 files
  • Unit suite — 1238 runs; 4 failures + 3 errors, all in builder_test/build_test, identical on pristine dash HEAD (host-arch dependent, pass in CI)
  • Generated command inspected by hand — all 7 deploy and 5 run flags present with the expected shapes
  • Deploy with cache.enabled: true against an app sending Cache-Control: public, max-age=60; confirm a second request is served from cache
  • Confirm a Set-Cookie response is refused with allow_set_cookie: false and stored with it true
  • Point two hosts at one redis:// store; confirm a fetch on one warms the other
  • Change run.cache.store and confirm exactly one proxy reboot per host

Automated coverage

Test Proves
enabled alone emits only --cache AC1 — defaults stay in the proxy; no key, no flag
durations use the seconds convention and sizes are byte counts 300 → 300s, 1_048_576 stays an integer
vary_headers and vary_cookies become repeated flags AC2 — repeated --cache-vary-header= pairs, checked against internal/cmd/deploy.go's StringSliceVar
negative lease durations survive the space-separated rendering -1s reaches the proxy intact rather than reading as the next flag
config_digest changes when the store changes AC3 — one reboot per host on a store change
no cache block leaves the deploy command unchanged / no run cache block leaves the run command unchanged AC4 — pure addition
policy keys without enabled are a config error / an unsupported store fails at config time the two silent failures above
every kamal-proxy deploy/run flag is exposed or waived all twelve are really wired, with the waivers gone

Deviations & judgment calls

Deviations from the issue

  • Twelve flags, not nine. The issue's prose lists 6 deploy + 3 run. The coverage canary's waiver for Expose the response cache in deploy.yml (policy per service, store proxy-wide) #75 lists 7 + 5 — --cache-max-variants, --cache-lease-ttl and --cache-lease-wait are also unexposed and attributed here. Exposing only the nine would leave the waiver half-populated with no issue left to attribute the rest to.
  • max_body: 1_048_576, not 1MB. The issue's example contradicts its own rule ("sizes follow whatever buffer-memory / max_request_body already accept" — those are plain integers). --cache-max-body is a Cobra Int64Var that would reject 1MB outright.
  • seconds_duration moved to Kamal::Utils rather than being copied into a second class.

Judgment calls

  • No Proxy::Cache class, unlike Proxy::Acme. Acme earned one — secrets resolution, an env file, a provider allowlist, credential names in the digest. The cache store is five scalars, so it follows the tls_options shape: a named private method merged into the options hash. Each file keeps its own local convention.
  • Cache options are NOT stripped when load balancing. Unlike TLS this is not a "where the handshake happens" concern, and the existing design already gives every non-TLS deploy option to both layers (buffer-requests, path-timeout, …). So a load-balanced deploy caches at the edge and again per host — a valid two-tier cache, but worth knowing before you tune max_body twice.
  • run.cache.store is a plain value, so a Redis URL with a password in it lands on the docker command line — host process listings and kamal's audit log. The CACHE_STORE env fallback leaks identically. First-class proxy.run.acme block with secrets-backed DNS credentials #73 built exactly the right machinery for this (a 0600 env file passed with --env-file), but generalizing it from "ACME credentials" to "proxy secrets" is a refactor of just-merged code that this issue did not ask for. The docs stub warns about it; worth its own issue.
  • The load balancer still gets no run flags, so run.cache.store does not reach it — the LB container runs the image's default CMD. Same pre-existing gap flagged in First-class proxy.run.acme block with secrets-backed DNS credentials #73 and Expose on-demand TLS, mTLS client CA and ACME cache path in deploy.yml #74.
  • store_timeout / lease_ttl / lease_wait are not validated against store. They only matter with a shared store, but the proxy ignores them harmlessly with memory, so this is not the silent-failure class the other two checks catch.

The RFC 9111 response cache was fully built in the proxy and completely
unreachable from deploy.yml. Twelve flags had no key.

    proxy:
      cache:
        enabled: true
        max_ttl: 300
        max_body: 1_048_576
        max_variants: 8
        vary_headers: [ Accept-Encoding ]
        vary_cookies: [ locale ]
        allow_set_cookie: false
      run:
        cache:
          store: redis://cache.example.com:6379/0
          store_timeout: 2
          memory_size: 134_217_728
          lease_ttl: 30
          lease_wait: 1

The split follows which kamal-proxy subcommand owns each flag: the policy is per
service and rides the deploy, the store is proxy-wide and rides the run command,
so changing it moves config_digest and reboots the proxy once.

Twelve flags rather than the nine the issue's prose lists. The coverage canary's
waiver for this issue also named --cache-max-variants, --cache-lease-ttl and
--cache-lease-wait; exposing only the nine would have left the waiver
half-populated with no issue left to attribute the rest to.

Sizes are plain byte integers, not "1MB" strings. The issue asked for sizes to
follow whatever buffering already accepts, which is integers, and --cache-max-body
is a Cobra Int64Var that would reject "1MB" anyway.

The lease flags take negative values to switch cross-node coalescing off. That
survives the space-separated rendering the run command uses -- pflag's
parseLongArg reads the argument after --flag unconditionally, with no
leading-dash check -- so unlike the ACME booleans these need no `=` group.

Two silent failures are now config errors: a tuned cache block with no
`enabled: true`, which kamal-proxy ignores wholesale, and a store that is
neither memory nor a redis(s):// URL, which kills the proxy container in preRun
rather than failing a deploy.

seconds_duration moved from a private method on Proxy to Kamal::Utils, since
Proxy::Run needs it now too.

Closes #75
@mhenrixon mhenrixon self-assigned this Jul 30, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Jul 30, 2026
@mhenrixon
mhenrixon merged commit 0b9efa9 into dash Jul 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose the response cache in deploy.yml (policy per service, store proxy-wide)

1 participant