Skip to content

Add idle container configuration for kamal-proxy#1800

Draft
martijnenco wants to merge 1 commit intobasecamp:mainfrom
martijnenco:feature/add-support-for-review-apps
Draft

Add idle container configuration for kamal-proxy#1800
martijnenco wants to merge 1 commit intobasecamp:mainfrom
martijnenco:feature/add-support-for-review-apps

Conversation

@martijnenco
Copy link
Copy Markdown

@martijnenco martijnenco commented Mar 12, 2026

Add idle container configuration for kamal-proxy

Description

Expose the new kamal-proxy idle container feature (scale-to-zero) through the Kamal deploy configuration.

What changed

Configuration (lib/kamal/configuration/proxy.rb):

  • New idle section under proxy with timeout and wake_timeout options.
  • idle? helper method for checking whether idle is configured.
  • deploy_options passes --idle-timeout and --idle-wake-timeout to kamal-proxy.

Docker socket mount (lib/kamal/configuration/proxy/run.rb):

  • When any role uses idle, the Docker socket is automatically mounted into the proxy container (--volume=/var/run/docker.sock:/var/run/docker.sock).
  • An explicit docker_socket: true option under proxy.run is also available for manual control.

Validation (lib/kamal/configuration/validator/proxy.rb):

  • idle.timeout and idle.wake_timeout must be positive integers if set.

Documentation (lib/kamal/configuration/docs/proxy.yml):

  • New idle and docker_socket sections with usage examples.

Tests:

  • Configuration tests for idle parsing and deploy option output.
  • Command tests verifying --idle-timeout / --idle-wake-timeout flags and Docker socket mount.

Usage

proxy:
  host: my-review-app.example.com
  app_port: 3000
  idle:
    timeout: 300        # Stop container after 5 minutes of no requests
    wake_timeout: 30    # Hold request up to 30s while container wakes

Why

Review apps and low-traffic environments waste server resources sitting idle. This configuration allows Kamal users to opt into automatic idle management with a single deploy.yml change -- no sidecar containers, no external tooling. The proxy handles everything: tracking inactivity, stopping the container, and transparently waking it when the next request arrives.

Dependent to this PR for kamal-proxy: basecamp/kamal-proxy#197

Copilot AI review requested due to automatic review settings March 12, 2026 09:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Exposes kamal-proxy’s idle container (scale-to-zero) feature through Kamal’s deploy configuration, including deploy-time flags and automatic Docker socket mounting for the proxy container.

Changes:

  • Add proxy.idle.timeout / proxy.idle.wake_timeout config, an idle? helper, and pass --idle-timeout / --idle-wake-timeout to kamal-proxy deploy.
  • Add proxy.run.docker_socket and attempt to auto-mount /var/run/docker.sock when idle is configured.
  • Add validation/docs/tests for the new settings.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/kamal/configuration/proxy.rb Adds idle? and includes idle flags in deploy_options.
lib/kamal/configuration/proxy/run.rb Adds Docker socket mount logic via docker_socket?.
lib/kamal/configuration/validator/proxy.rb Adds validation for idle config values.
lib/kamal/configuration.rb Adds any_role_use_proxy_idle? helper used by proxy run config.
lib/kamal/configuration/docs/proxy.yml Documents idle and run.docker_socket settings.
test/configuration/proxy_test.rb Adds tests for idle parsing and validation.
test/commands/app_test.rb Adds a test ensuring idle flags are included in deploy command output.
test/commands/proxy_test.rb Adds tests for Docker socket mount behavior under proxy run configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +30
if config["idle"]["timeout"].present? && !config["idle"]["timeout"].is_a?(Integer)
error "Idle timeout must be an integer (seconds)"
end

if config["idle"]["wake_timeout"].present? && !config["idle"]["wake_timeout"].is_a?(Integer)
error "Idle wake timeout must be an integer (seconds)"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new idle validation only checks that the values are Integers, but it does not enforce the documented/PR-stated requirement that idle.timeout and idle.wake_timeout be positive (e.g., 0 or negative values would pass and produce "0s"/"-5s" durations later). Consider validating > 0 (and possibly adding a targeted error message) for any configured idle timeout values.

Suggested change
if config["idle"]["timeout"].present? && !config["idle"]["timeout"].is_a?(Integer)
error "Idle timeout must be an integer (seconds)"
end
if config["idle"]["wake_timeout"].present? && !config["idle"]["wake_timeout"].is_a?(Integer)
error "Idle wake timeout must be an integer (seconds)"
if config["idle"]["timeout"].present?
if !config["idle"]["timeout"].is_a?(Integer)
error "Idle timeout must be an integer (seconds)"
elsif config["idle"]["timeout"] <= 0
error "Idle timeout must be a positive integer (seconds)"
end
end
if config["idle"]["wake_timeout"].present?
if !config["idle"]["wake_timeout"].is_a?(Integer)
error "Idle wake timeout must be an integer (seconds)"
elsif config["idle"]["wake_timeout"] <= 0
error "Idle wake timeout must be a positive integer (seconds)"
end

Copilot uses AI. Check for mistakes.
end

def idle?
proxy_config.dig("idle", "timeout").present?
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idle? only checks idle.timeout. If a user configures only idle.wake_timeout, idle? will be false (which also affects any_role_use_proxy_idle?) even though deploy_options may still emit an idle-related flag. Consider either treating wake_timeout as enabling idle as well, or validating that wake_timeout cannot be set without timeout.

Suggested change
proxy_config.dig("idle", "timeout").present?
idle_config = proxy_config["idle"]
idle_config.is_a?(Hash) && (idle_config["timeout"].present? || idle_config["wake_timeout"].present?)

Copilot uses AI. Check for mistakes.
Comment on lines 96 to 99
"log-response-header": proxy_config.dig("logging", "response_headers"),
"idle-timeout": seconds_duration(proxy_config.dig("idle", "timeout")),
"idle-wake-timeout": seconds_duration(proxy_config.dig("idle", "wake_timeout")),
"error-pages": error_pages
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploy_options will include idle-wake-timeout whenever idle.wake_timeout is set, even if idle.timeout is missing/disabled. That can produce a deploy command with a wake-timeout flag but no idle-timeout, and (depending on how the proxy container is started) without Docker socket access. Consider only emitting idle-wake-timeout when idle.timeout is configured, or enforce the dependency in validation.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +22
def docker_socket?
run_config.fetch("docker_socket", config.any_role_use_proxy_idle?)
end
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Docker socket auto-mount is implemented via Proxy::Run#docker_socket?, but Proxy::Run is only instantiated/used when a per-host proxy.run config exists. When proxy.run is not configured (the default path in Kamal::Commands::Proxy#run uses the boot-file pipeline), idle can be enabled via proxy.idle but the proxy container still won't get the Docker socket mount. If the intent is that setting proxy.idle alone enables scale-to-zero, this needs to also affect the boot-mode run options (or the docs should explicitly require proxy.run).

Copilot uses AI. Check for mistakes.
metrics_port: 9090 # Port for Prometheus metrics
debug: true # Debug logging (default: false)
log_max_size: "30m" # Maximum log file size (default: "10m")
docker_socket: true # Mount the Docker socket (default: false, or true if idle is used)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment for run.docker_socket says the default is "false, or true if idle is used", but the current implementation only defaults to true when the proxy is started via a proxy.run config (i.e., when Proxy::Run is used). If the proxy is started via the boot-file path (no proxy.run), proxy.idle currently won’t imply a Docker socket mount. Please either adjust the docs to reflect this limitation or update the boot-mode run options to match the documented default.

Suggested change
docker_socket: true # Mount the Docker socket (default: false, or true if idle is used)
docker_socket: true # Mount the Docker socket (default: false; may default to true with idle when using proxy.run)

Copilot uses AI. Check for mistakes.
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $PWD/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config --volume=/var/run/docker.sock:/var/run/docker.sock --publish 80:80 --publish 443:443 --log-opt max-size=10m basecamp/kamal-proxy:v0.9.2 kamal-proxy run",
new_command.run.join(" ")
end

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test covers the Docker socket mount when proxy.idle is set and proxy.run is configured (so the code path uses proxy_run_config). Given the intended behavior in the PR description/docs, it would also be useful to add a regression test for proxy.idle without any proxy.run config to ensure the boot-mode run command still mounts /var/run/docker.sock (or to lock in the requirement that proxy.run must be configured).

Suggested change
test "docker socket mount when idle configured without run config" do
@config[:proxy] = { "idle" => { "timeout" => 300 } }
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $PWD/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config --volume=/var/run/docker.sock:/var/run/docker.sock --publish 80:80 --publish 443:443 --log-opt max-size=10m basecamp/kamal-proxy:v0.9.2 kamal-proxy run",
new_command.run.join(" ")
end

Copilot uses AI. Check for mistakes.
}
}

assert_raises(Kamal::ConfigurationError) { config.proxy }
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added validation test covers type-checking (string vs integer), but it doesn't cover the documented requirement that idle timeouts be positive. If the validator is updated to reject 0/negative values, consider adding assertions here for those cases to prevent regressions.

Suggested change
assert_raises(Kamal::ConfigurationError) { config.proxy }
assert_raises(Kamal::ConfigurationError) { config.proxy }
@deploy[:proxy] = {
"host" => "example.com",
"idle" => {
"timeout" => 0
}
}
assert_raises(Kamal::ConfigurationError) { config.proxy }
@deploy[:proxy] = {
"host" => "example.com",
"idle" => {
"timeout" => -1
}
}
assert_raises(Kamal::ConfigurationError) { config.proxy }

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants