diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 873cf6657..b07f73de9 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -37,13 +37,25 @@ def deploy(boot_accessories: false) say "Ensure kamal-proxy is running...", :magenta invoke "kamal:cli:proxy:boot", [], invoke_options - invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories + # Boot non-proxied accessories first to avoid error pages directory validation issues on fresh servers. + # Proxied accessories will be booted after app:boot to ensure error pages are available. + # The outer deploy lock remains held across both accessory groups and app:boot. + if boot_accessories + non_proxied_accessories = KAMAL.accessory_names.filter { |name| !KAMAL.accessory(name).running_proxy? } + non_proxied_accessories.each { |name| invoke "kamal:cli:accessory:boot", [ name ], invoke_options } + end say "Detect stale containers...", :magenta invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) invoke "kamal:cli:app:boot", [], invoke_options + # Boot proxied accessories after app:boot to ensure error pages directory exists + if boot_accessories + proxied_accessories = KAMAL.accessory_names.filter { |name| KAMAL.accessory(name).running_proxy? } + proxied_accessories.each { |name| invoke "kamal:cli:accessory:boot", [ name ], invoke_options } + end + say "Prune old containers and images...", :magenta invoke "kamal:cli:prune:all", [], invoke_options end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 16b2807e2..353258300 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -19,8 +19,7 @@ class CliMainTest < CliTestCase invoke_options = base_invoke_options Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options) - # deploy + # deploy (deploy_simple.yml has no accessories, so no accessory:boot invocations expected) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) @@ -67,8 +66,7 @@ class CliMainTest < CliTestCase invoke_options = base_invoke_options(no_cache: true) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options) - # deploy + # deploy (deploy_simple.yml has no accessories, so no accessory:boot invocations expected) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) @@ -85,6 +83,30 @@ class CliMainTest < CliTestCase end end + test "setup with error pages and proxied accessory boots accessories in correct order" do + invoke_options = base_invoke_options(config_file: "deploy_with_error_pages_and_accessories.yml") + boot_sequence = sequence("boot accessories around app") + + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options) + # Non-proxied accessory (mysql) should be booted first + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "mysql" ], invoke_options).in_sequence(boot_sequence) + # deploy + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options).in_sequence(boot_sequence) + # Proxied accessory (cache) should be booted after app:boot to ensure error pages directory exists + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "cache" ], invoke_options).in_sequence(boot_sequence) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) + + run_command("setup", config_file: "deploy_with_error_pages_and_accessories.yml").tap do |output| + assert_match /Ensure Docker is installed.../, output + assert_match /Build and push app image/, output + assert_match /Ensure kamal-proxy is running/, output + assert_match /Prune old containers and images/, output + end + end + test "deploy" do with_test_secrets("secrets" => "DB_PASSWORD=secret") do invoke_options = base_invoke_options(verbose: true) diff --git a/test/fixtures/deploy_with_error_pages_and_accessories.yml b/test/fixtures/deploy_with_error_pages_and_accessories.yml new file mode 100644 index 000000000..fc29a6bd5 --- /dev/null +++ b/test/fixtures/deploy_with_error_pages_and_accessories.yml @@ -0,0 +1,25 @@ +service: app +image: dhh/app +servers: + - "1.1.1.1" +registry: + username: user + password: pw +builder: + arch: amd64 + +accessories: + mysql: + image: mysql:5.7 + host: 1.1.1.1 + port: 3306 + cache: + image: redis:7-alpine + host: 1.1.1.1 + port: 6379:6379 + proxy: + host: cache.example.com + path_prefix: "/cache" + app_port: 6379 + +error_pages_path: public