From bb2f70ac3b8ef96dacd6750461701702a381a0f8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:53:00 +0200 Subject: [PATCH] fix(railpack): isolate buildx from Docker client env (#10840) --- app/Jobs/ApplicationDeploymentJob.php | 63 ++++++++++++++++- ...pplicationDeploymentRailpackConfigTest.php | 69 +++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 19cd8ef67..3e415c729 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -52,6 +52,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private const RAILPACK_GENERATED_CONFIG_PATH = '.coolify/railpack.generated.json'; + private const DOCKER_CLIENT_ENV_KEYS = [ + 'BUILDKIT_HOST', + 'BUILDX_BUILDER', + 'BUILDX_CONFIG', + 'DOCKER_API_VERSION', + 'DOCKER_BUILDKIT', + 'DOCKER_CERT_PATH', + 'DOCKER_CLI_EXPERIMENTAL', + 'DOCKER_CONFIG', + 'DOCKER_CONTEXT', + 'DOCKER_HOST', + 'DOCKER_TLS', + 'DOCKER_TLS_VERIFY', + ]; + public $tries = 1; public $timeout = 3600; @@ -1701,6 +1716,10 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { + if ($this->build_pack === 'railpack' && $this->is_reserved_docker_client_env_key($env->key)) { + continue; + } + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); // For literal/multiline vars, real_value includes quotes that we need to remove if ($env->is_literal || $env->is_multiline) { @@ -1752,6 +1771,10 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { + if ($this->build_pack === 'railpack' && $this->is_reserved_docker_client_env_key($env->key)) { + continue; + } + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); // For literal/multiline vars, real_value includes quotes that we need to remove if ($env->is_literal || $env->is_multiline) { @@ -2589,6 +2612,20 @@ private function generate_nixpacks_env_variables() $this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); } + private function is_reserved_docker_client_env_key(?string $key): bool + { + if (blank($key)) { + return false; + } + + return in_array(strtoupper($key), self::DOCKER_CLIENT_ENV_KEYS, true); + } + + private function without_reserved_docker_client_variables(Collection $variables): Collection + { + return $variables->reject(fn ($value, $key) => $this->is_reserved_docker_client_env_key((string) $key)); + } + private function generate_railpack_env_variables(): Collection { $variables = $this->railpack_build_variables(); @@ -2709,6 +2746,8 @@ private function railpack_build_secret_flags(Collection $variables): string private function railpack_build_command(string $imageName, Collection $variables): string { + $variables = $this->without_reserved_docker_client_variables($variables); + $cacheArgs = ''; if ($this->force_rebuild) { $cacheArgs = '--no-cache'; @@ -2735,7 +2774,7 @@ private function railpack_build_command(string $imageName, Collection $variables $secretFlags = $this->railpack_build_secret_flags($variables); $frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version'); - $buildxBuildCommand = "{$environmentPrefix}docker buildx build --builder coolify-railpack" + $buildxBuildCommand = "{$environmentPrefix}DOCKER_CONFIG=/root/.docker docker buildx build --builder coolify-railpack" ." {$this->addHosts} --network host" ." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\"" ." {$cacheArgs}" @@ -2746,7 +2785,7 @@ private function railpack_build_command(string $imageName, Collection $variables ." -t {$imageName}" ." {$this->workdir}"; - return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true' + return 'DOCKER_CONFIG=/root/.docker docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true' .' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand); } @@ -2901,9 +2940,25 @@ private function ensure_docker_buildx_available_for_railpack(): void throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin on the build server. Install or enable docker buildx and retry the deployment.'); } + private function ensure_helper_docker_buildx_available_for_railpack(): void + { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, 'DOCKER_CONFIG=/root/.docker docker buildx version >/dev/null 2>&1 && echo available || echo not-available'), + 'hidden' => true, + 'save' => 'railpack_helper_buildx_available', + ]); + + if (trim((string) $this->saved_outputs->get('railpack_helper_buildx_available')) === 'available') { + return; + } + + throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin inside the Coolify helper container. The helper could not find buildx at /root/.docker/cli-plugins/docker-buildx. Pull the latest helper image and retry the deployment.'); + } + private function build_railpack_image(): void { $this->ensure_docker_buildx_available_for_railpack(); + $this->ensure_helper_docker_buildx_available_for_railpack(); $railpackVariables = $this->generate_railpack_env_variables(); $railpackConfigPath = $this->generate_railpack_config_file(); @@ -4103,6 +4158,10 @@ private function generate_docker_env_flags_for_secrets() $variables = $this->env_args; + if ($this->build_pack === 'railpack') { + $variables = $this->without_reserved_docker_client_variables($variables); + } + if ($variables->isEmpty()) { return ''; } diff --git a/tests/Unit/ApplicationDeploymentRailpackConfigTest.php b/tests/Unit/ApplicationDeploymentRailpackConfigTest.php index c67761faa..03822cfd2 100644 --- a/tests/Unit/ApplicationDeploymentRailpackConfigTest.php +++ b/tests/Unit/ApplicationDeploymentRailpackConfigTest.php @@ -218,6 +218,75 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $ ->toThrow(DeploymentException::class, 'Railpack deployments require the Docker buildx CLI plugin'); }); +it('checks buildx inside the helper container before railpack builds', function () { + [$job, $reflection] = makeRailpackDeploymentJob([], [ + 'railpack_helper_buildx_available' => 'available', + ]); + + invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack'); + + expect($job->recordedCommands[0][0][0]) + ->toContain('DOCKER_CONFIG=/root/.docker docker buildx version'); +}); + +it('fails clearly when buildx is missing inside the helper container', function () { + [$job, $reflection] = makeRailpackDeploymentJob([], [ + 'railpack_helper_buildx_available' => 'not-available', + ]); + + expect(fn () => invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack')) + ->toThrow(DeploymentException::class, 'helper container'); +}); + +it('pins docker config while running railpack buildx commands', function () { + [$job, $reflection] = makeRailpackDeploymentJob([ + 'uuid' => 'application-uuid', + ]); + + $command = invokeRailpackMethod( + $job, + $reflection, + 'railpack_build_command', + [ + 'coollabsio/coolify:test', + collect([]), + ], + ); + + expect($command) + ->toContain('DOCKER_CONFIG=/root/.docker docker buildx create --name coolify-railpack') + ->toContain('DOCKER_CONFIG=/root/.docker docker buildx build --builder coolify-railpack'); +}); + +it('filters reserved docker client variables from railpack build secrets', function () { + [$job, $reflection] = makeRailpackDeploymentJob([ + 'uuid' => 'application-uuid', + ]); + + $command = invokeRailpackMethod( + $job, + $reflection, + 'railpack_build_command', + [ + 'coollabsio/coolify:test', + collect([ + 'DOCKER_CONFIG' => '/tmp/no-buildx', + 'DOCKER_HOST' => 'tcp://invalid:2375', + 'RAILPACK_NODE_VERSION' => '22', + 'APP_ENV' => 'production', + ]), + ], + ); + + expect($command) + ->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'") + ->toContain("--secret 'id=APP_ENV,env=APP_ENV'") + ->not->toContain('id=DOCKER_CONFIG') + ->not->toContain('id=DOCKER_HOST') + ->not->toContain("env 'DOCKER_CONFIG=") + ->not->toContain("env 'DOCKER_HOST="); +}); + it('builds railpack docker command with matching env and secret flags for all railpack variables', function () { [$job, $reflection] = makeRailpackDeploymentJob([ 'uuid' => 'application-uuid',