From efbbe76310ee1ba288a64604315d051a2477cadd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:16:01 +0200 Subject: [PATCH 01/10] feat(deployment): add support for Docker BuildKit and build secrets to enhance security and flexibility during application deployment refactor(static-buildpack): seperate static buildpack for readability --- app/Jobs/ApplicationDeploymentJob.php | 670 +++++++++++++++++++++----- 1 file changed, 551 insertions(+), 119 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 207164ec0..192099bb3 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -167,6 +167,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $preserveRepository = false; + private bool $dockerBuildkitSupported = false; + + private Collection|string $build_secrets; + + private string $secrets_dir = ''; + public function tags() { // Do not remove this one, it needs to properly identify which worker is running the job @@ -183,6 +189,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application = Application::find($this->application_deployment_queue->application_id); $this->build_pack = data_get($this->application, 'build_pack'); $this->build_args = collect([]); + $this->build_secrets = ''; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->pull_request_id = $this->application_deployment_queue->pull_request_id; @@ -272,6 +279,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); + // Check Docker Version + $this->checkDockerVersion(); + // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); @@ -344,6 +354,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); } + + // Cleanup build secrets if they were used + $this->cleanup_build_secrets(); + $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -351,6 +365,47 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } + private function checkDockerVersion(): void + { + // Use the build server if available, otherwise use the deployment server + $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; + + try { + // Check Docker version (BuildKit requires Docker 18.09+) + $dockerVersion = instant_remote_process( + ["docker version --format '{{.Server.Version}}'"], + $serverToCheck + ); + + // Parse version and check if >= 18.09 + $versionParts = explode('.', $dockerVersion); + $majorVersion = (int) $versionParts[0]; + $minorVersion = (int) ($versionParts[1] ?? 0); + + if ($majorVersion > 18 || ($majorVersion == 18 && $minorVersion >= 9)) { + // Test if BuildKit is available with secrets support + $buildkitTest = instant_remote_process( + ["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], + $serverToCheck + ); + + if (trim($buildkitTest) === 'supported') { + $this->dockerBuildkitSupported = true; + $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; + $this->application_deployment_queue->addLogEntry("Docker BuildKit with secrets support detected on {$serverName}. Build secrets will be used for enhanced security."); + } else { + $this->application_deployment_queue->addLogEntry('Docker BuildKit secrets not available. Falling back to build arguments.'); + } + } else { + $this->application_deployment_queue->addLogEntry("Docker version {$dockerVersion} detected. BuildKit requires 18.09+. Using build arguments."); + } + } catch (\Exception $e) { + // If check fails, default to false + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry('Could not determine Docker BuildKit support. Using build arguments as fallback.'); + } + } + private function decide_what_to_do() { if ($this->restart_only) { @@ -479,11 +534,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } $this->generate_image_names(); $this->cleanup_git(); + + // Check for BuildKit support and generate build secrets + $this->checkDockerVersion(); + $this->generate_build_env_variables(); + $this->application->loadComposeFile(isInit: false); if ($this->application->settings->is_raw_compose_deployment_enabled) { $this->application->oldRawParser(); $yaml = $composeFile = $this->application->docker_compose_raw; $this->save_environment_variables(); + + // For raw compose, we cannot automatically add secrets configuration + // User must define it manually in their docker-compose file + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.'); + } } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); @@ -502,6 +568,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return; } + + // Add build secrets to compose file if BuildKit is supported + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $composeFile = $this->add_build_secrets_to_compose($composeFile); + } + $yaml = Yaml::dump(convertToArray($composeFile), 10); } $this->docker_compose_base64 = base64_encode($yaml); @@ -513,11 +585,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); if ($this->docker_compose_custom_build_command) { + // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported + $build_command = $this->docker_compose_custom_build_command; + if ($this->dockerBuildkitSupported) { + $build_command = "DOCKER_BUILDKIT=1 {$build_command}"; + } $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_build_command}"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true], ); } else { $command = "{$this->coolify_variables} docker compose"; + // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported + if ($this->dockerBuildkitSupported) { + $command = "DOCKER_BUILDKIT=1 {$command}"; + } if (filled($this->env_filename)) { $command .= " --env-file {$this->workdir}/{$this->env_filename}"; } @@ -531,6 +612,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ); } + // Cleanup build secrets after build completes + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->cleanup_build_secrets(); + } + $this->stop_running_container(force: true); $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; @@ -616,6 +702,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); + $this->checkDockerVersion(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -630,6 +717,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + $this->cleanup_build_secrets(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -677,7 +765,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->clone_repository(); $this->cleanup_git(); $this->generate_compose_file(); - $this->build_image(); + $this->build_static_image(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -2136,16 +2224,72 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ); } + private function build_static_image() + { + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); + if ($this->application->static_image) { + $this->pull_latest_image($this->application->static_image); + } + $dockerfile = base64_encode("FROM {$this->application->static_image} + WORKDIR /usr/share/nginx/html/ + LABEL coolify.deploymentId={$this->deployment_uuid} + COPY . . + RUN rm -f /usr/share/nginx/html/nginx.conf + RUN rm -f /usr/share/nginx/html/Dockerfile + RUN rm -f /usr/share/nginx/html/docker-compose.yaml + RUN rm -f /usr/share/nginx/html/.env + COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); + } + } + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}"; + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + $this->application_deployment_queue->addLogEntry('Building docker image completed.'); + } + private function build_image() { - // Add Coolify related variables to the build args - $this->environment_variables->filter(function ($key, $value) { - return str($key)->startsWith('COOLIFY_'); - })->each(function ($key, $value) { - $this->build_args->push("--build-arg '{$key}'"); - }); + // Add Coolify related variables to the build args/secrets + if ($this->dockerBuildkitSupported) { + // Coolify variables are already included in the secrets from generate_build_env_variables + // build_secrets is already a string at this point + } else { + // Traditional build args approach + $this->environment_variables->filter(function ($key, $value) { + return str($key)->startsWith('COOLIFY_'); + })->each(function ($key, $value) { + $this->build_args->push("--build-arg '{$key}'"); + }); - $this->build_args = $this->build_args->implode(' '); + $this->build_args = $this->build_args->implode(' '); + } $this->application_deployment_queue->addLogEntry('----------------------------------------'); if ($this->disableBuildCache) { @@ -2158,106 +2302,110 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry('To check the current progress, click on Show Debug Logs.'); } - if ($this->application->settings->is_static || $this->application->build_pack === 'static') { + if ($this->application->settings->is_static) { if ($this->application->static_image) { $this->pull_latest_image($this->application->static_image); $this->application_deployment_queue->addLogEntry('Continuing with the building process.'); } - if ($this->application->build_pack === 'static') { - $dockerfile = base64_encode("FROM {$this->application->static_image} -WORKDIR /usr/share/nginx/html/ -LABEL coolify.deploymentId={$this->deployment_uuid} -COPY . . -RUN rm -f /usr/share/nginx/html/nginx.conf -RUN rm -f /usr/share/nginx/html/Dockerfile -RUN rm -f /usr/share/nginx/html/docker-compose.yaml -RUN rm -f /usr/share/nginx/html/.env -COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { - $nginx_config = base64_encode($this->application->custom_nginx_configuration); - } else { - if ($this->application->settings->is_spa) { - $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + if ($this->application->build_pack === 'nixpacks') { + $this->nixpacks_plan = base64_encode($this->nixpacks_plan); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); + if ($this->force_rebuild) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, + ]); + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); - } - } - } else { - if ($this->application->build_pack === 'nixpacks') { - $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); - if ($this->force_rebuild) { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), - 'hidden' => true, - ], [ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), - 'hidden' => true, - ]); $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; + } + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, + ]); + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), - 'hidden' => true, - ], [ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), - 'hidden' => true, - ]); $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } + } - $base64_build_command = base64_encode($build_command); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), - 'hidden' => true, - ] - ); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); + } else { + // Dockerfile buildpack + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"; + } } else { + // Traditional build with args if ($this->force_rebuild) { $build_command = "docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"; - $base64_build_command = base64_encode($build_command); } else { $build_command = "docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"; - $base64_build_command = base64_encode($build_command); } - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), - 'hidden' => true, - ] - ); } - $dockerfile = base64_encode("FROM {$this->application->static_image} + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + } + $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { - $nginx_config = base64_encode($this->application->custom_nginx_configuration); + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); } else { - if ($this->application->settings->is_spa) { - $nginx_config = base64_encode(defaultNginxConfiguration('spa')); - } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); - } + $nginx_config = base64_encode(defaultNginxConfiguration()); } } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; @@ -2285,10 +2433,21 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } else { // Pure Dockerfile based deployment if ($this->application->dockerfile) { - if ($this->force_rebuild) { - $build_command = "docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets (only if secrets exist) + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } else { - $build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + // Traditional build with args + if ($this->force_rebuild) { + $build_command = "docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( @@ -2317,7 +2476,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + } } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2326,7 +2492,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + } } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( @@ -2345,13 +2518,24 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { - if ($this->force_rebuild) { - $build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; - $base64_build_command = base64_encode($build_command); + // Dockerfile buildpack + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } else { - $build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; - $base64_build_command = base64_encode($build_command); + // Traditional build with args + if ($this->force_rebuild) { + $build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } + $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2447,14 +2631,108 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $variables = collect([])->merge($this->env_args); } - $this->build_args = $variables->map(function ($value, $key) { - $value = escapeshellarg($value); + if ($this->dockerBuildkitSupported) { + // Generate build secrets instead of build args + $this->generate_build_secrets($variables); + // Ensure build_args is empty string when using secrets + $this->build_args = ''; + } else { + // Fallback to traditional build args + $this->build_args = $variables->map(function ($value, $key) { + $value = escapeshellarg($value); - return "--build-arg {$key}={$value}"; - }); + return "--build-arg {$key}={$value}"; + }); + } + } + + private function generate_build_secrets(Collection $variables) + { + $this->build_secrets = collect([]); + + // Only create secrets if there are variables to process + if ($variables->isEmpty()) { + $this->build_secrets = ''; + + return; + } + + $this->secrets_dir = "/tmp/.build_secrets_{$this->deployment_uuid}"; + + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "mkdir -p {$this->secrets_dir}" + ), 'hidden' => true]); + + // Generate a secret file for each environment variable + foreach ($variables as $key => $value) { + $secret_file = "{$this->secrets_dir}/{$key}"; + $escaped_value = base64_encode($value); + + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "echo '{$escaped_value}' | base64 -d > {$secret_file} && chmod 600 {$secret_file}" + ), 'hidden' => true]); + + $this->build_secrets->push("--secret id={$key},src={$secret_file}"); + } + + $this->build_secrets = $this->build_secrets->implode(' '); + } + + private function cleanup_build_secrets() + { + if ($this->dockerBuildkitSupported && $this->secrets_dir) { + // Clean up the secrets directory from the host + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "rm -rf {$this->secrets_dir}", + ), 'hidden' => true, 'ignore_errors' => true]); + } } private function add_build_env_variables_to_dockerfile() + { + if ($this->dockerBuildkitSupported) { + // When using BuildKit, we need to add the syntax directive and instructions on how to use secrets + $this->add_buildkit_secrets_to_dockerfile(); + } else { + // Traditional approach - add ARGs to the Dockerfile + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'save' => 'dockerfile', + ]); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Include ALL environment variables as build args (deprecating is_build_time flag) + if ($this->pull_request_id === 0) { + // Get all environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + } else { + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + } + } + } else { + // Get all preview environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + } else { + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + } + } + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + 'hidden' => true, + ]); + } + } + + private function add_buildkit_secrets_to_dockerfile() { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), @@ -2463,28 +2741,55 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - // Include ALL environment variables as build args (deprecating is_build_time flag) - if ($this->pull_request_id === 0) { - // Get all environment variables except NIXPACKS_ prefixed ones - $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Check if BuildKit syntax is already present + $firstLine = $dockerfile->first(); + if (! str_starts_with($firstLine, '# syntax=')) { + // Add BuildKit syntax directive at the very beginning + $dockerfile->prepend('# syntax=docker/dockerfile:1'); + } + + // Create a comment block explaining how to use the secrets in RUN commands + $secretsComment = [ + '', + '# Build secrets are available. Use them in RUN commands like:', + '# For a single secret (inline environment variable):', + '# RUN --mount=type=secret,id=MY_SECRET MY_SECRET=$(cat /run/secrets/MY_SECRET) npm run build', + '', + '# For multiple secrets (inline environment variables):', + '# RUN --mount=type=secret,id=API_KEY --mount=type=secret,id=DB_URL \\', + '# API_KEY=$(cat /run/secrets/API_KEY) \\', + '# DB_URL=$(cat /run/secrets/DB_URL) \\', + '# npm run build', + '', + '# Note: Do NOT use export. Variables are set inline for the specific command only.', + '', + ]; + + // Get the environment variables to document which secrets are available + $envs = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($envs->count() > 0) { + $secretsComment[] = '# Available secrets:'; foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); - } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); - } + $secretsComment[] = "# - {$env->key}"; } - } else { - // Get all preview environment variables except NIXPACKS_ prefixed ones - $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); - } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); - } + $secretsComment[] = ''; + } + + // Find where to insert the comments (after FROM statement) + $fromIndex = $dockerfile->search(function ($line) { + return str_starts_with(trim(strtoupper($line)), 'FROM'); + }); + + if ($fromIndex !== false) { + // Insert comments after FROM statement + foreach (array_reverse($secretsComment) as $comment) { + $dockerfile->splice($fromIndex + 1, 0, [$comment]); } } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), @@ -2492,6 +2797,133 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ]); } + private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) + { + // Only process if we have secrets to mount + if (empty($this->build_secrets)) { + return; + } + + // Read the nixpacks-generated Dockerfile + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$dockerfile_path}"), + 'hidden' => true, + 'save' => 'nixpacks_dockerfile', + ]); + + $dockerfile = collect(str($this->saved_outputs->get('nixpacks_dockerfile'))->trim()->explode("\n")); + + // Add BuildKit syntax directive if not present + $firstLine = $dockerfile->first(); + if (! str_starts_with($firstLine, '# syntax=')) { + $dockerfile->prepend('# syntax=docker/dockerfile:1'); + } + + // Get the list of available secrets + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + // Find all RUN commands and add secret mounts to them + $modified = false; + $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { + // Check if this is a RUN command + if (str_starts_with(trim($line), 'RUN')) { + + // Build the mount flags for all secrets + $mounts = []; + foreach ($variables as $env) { + $mounts[] = "--mount=type=secret,id={$env->key}"; + } + + if (! empty($mounts)) { + // Build inline environment variable assignments (no export) + $envAssignments = []; + foreach ($variables as $env) { + $envAssignments[] = "{$env->key}=\$(cat /run/secrets/{$env->key})"; + } + + // Replace RUN with RUN with mounts and inline env vars + $mountString = implode(' ', $mounts); + $envString = implode(' ', $envAssignments); + + // Extract the original command + $originalCommand = trim(substr($line, 3)); // Remove 'RUN' + + // Create the new RUN command with mounts and inline environment variables + // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command + $line = "RUN {$mountString} {$envString} {$originalCommand}"; + $modified = true; + } + } + + return $line; + }); + + if ($modified) { + // Write the modified Dockerfile back + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$dockerfile_path} > /dev/null"), + 'hidden' => true, + ]); + + $this->application_deployment_queue->addLogEntry('Modified Dockerfile to use build secrets: '.$dockerfile->implode("\n"), hidden: true); + } + } + + private function add_build_secrets_to_compose($composeFile) + { + // Get environment variables for secrets + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($variables->isEmpty()) { + return $composeFile; + } + + // Add top-level secrets definition + $secrets = []; + foreach ($variables as $env) { + $secrets[$env->key] = [ + 'file' => "{$this->secrets_dir}/{$env->key}", + ]; + } + + // Add build.secrets to services that have a build context + $services = data_get($composeFile, 'services', []); + foreach ($services as $serviceName => &$service) { + // Only add secrets if the service has a build context defined + if (isset($service['build'])) { + // Handle both string and array build configurations + if (is_string($service['build'])) { + // Convert string build to array format + $service['build'] = [ + 'context' => $service['build'], + ]; + } + // Add secrets to build configuration + if (! isset($service['build']['secrets'])) { + $service['build']['secrets'] = []; + } + foreach ($variables as $env) { + if (! in_array($env->key, $service['build']['secrets'])) { + $service['build']['secrets'][] = $env->key; + } + } + } + } + + // Update the compose file + $composeFile['services'] = $services; + $composeFile['secrets'] = $secrets; + + $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); + + return $composeFile; + } + private function run_pre_deployment_command() { if (empty($this->application->pre_deployment_command)) { From c182cac032294b338db659d3daca3fa487d5e97d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:20:36 +0200 Subject: [PATCH 02/10] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 192099bb3..497dd160d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2665,14 +2665,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); // Generate a secret file for each environment variable foreach ($variables as $key => $value) { - $secret_file = "{$this->secrets_dir}/{$key}"; + // keep id as-is, sanitize only filename + $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $key); + $secret_file_path = "{$this->secrets_dir}/{$safe_filename}"; $escaped_value = base64_encode($value); $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "echo '{$escaped_value}' | base64 -d > {$secret_file} && chmod 600 {$secret_file}" + "echo '{$escaped_value}' | base64 -d > {$secret_file_path} && chmod 600 {$secret_file_path}" ), 'hidden' => true]); - $this->build_secrets->push("--secret id={$key},src={$secret_file}"); + $this->build_secrets->push("--secret id={$key},src={$secret_file_path}"); } $this->build_secrets = $this->build_secrets->implode(' '); From 8542d33a2dc0c2151c100cabec0d7adb77c83700 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:20:51 +0200 Subject: [PATCH 03/10] refactor(deployment): conditionally cleanup build secrets based on Docker BuildKit support and remove redundant calls for improved efficiency --- app/Jobs/ApplicationDeploymentJob.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 497dd160d..456d63f96 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -355,8 +355,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->write_deployment_configurations(); } - // Cleanup build secrets if they were used - $this->cleanup_build_secrets(); + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->cleanup_build_secrets(); + } $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -612,11 +613,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ); } - // Cleanup build secrets after build completes - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { - $this->cleanup_build_secrets(); - } - $this->stop_running_container(force: true); $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; @@ -717,7 +713,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); - $this->cleanup_build_secrets(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -2288,7 +2283,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->build_args->push("--build-arg '{$key}'"); }); - $this->build_args = $this->build_args->implode(' '); + $this->build_args = $this->build_args instanceof \Illuminate\Support\Collection + ? $this->build_args->implode(' ') + : (string) $this->build_args; } $this->application_deployment_queue->addLogEntry('----------------------------------------'); From 6314fef8df9c5a90c44f519e8a62eeec7b45ae26 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:25:07 +0200 Subject: [PATCH 04/10] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 456d63f96..8851577e0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2826,8 +2826,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); // Find all RUN commands and add secret mounts to them $modified = false; $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { - // Check if this is a RUN command - if (str_starts_with(trim($line), 'RUN')) { + $trim = ltrim($line); + // Only handle shell-form RUN; skip JSON-form and already-mounted lines + if (str_starts_with($trim, 'RUN') && !preg_match('/^RUN\s*\[/i', $trim) && !str_contains($line, '--mount=type=secret')) { // Build the mount flags for all secrets $mounts = []; @@ -2847,7 +2848,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $envString = implode(' ', $envAssignments); // Extract the original command - $originalCommand = trim(substr($line, 3)); // Remove 'RUN' + $originalCommand = trim(substr($trim, 3)); // Remove 'RUN' // Create the new RUN command with mounts and inline environment variables // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command From f084ded6e9e8c79cafc4548525ac7a9ee0259829 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:25:16 +0200 Subject: [PATCH 05/10] refactor(deployment): remove redundant environment variable documentation from Dockerfile comments to streamline the deployment process --- app/Jobs/ApplicationDeploymentJob.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8851577e0..b8656e14a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2764,19 +2764,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); '', ]; - // Get the environment variables to document which secrets are available - $envs = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - - if ($envs->count() > 0) { - $secretsComment[] = '# Available secrets:'; - foreach ($envs as $env) { - $secretsComment[] = "# - {$env->key}"; - } - $secretsComment[] = ''; - } - // Find where to insert the comments (after FROM statement) $fromIndex = $dockerfile->search(function ($line) { return str_starts_with(trim(strtoupper($line)), 'FROM'); From f5e17337f40f8bc03eb89657cac81b1a8ee6d2f7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:26:12 +0200 Subject: [PATCH 06/10] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index b8656e14a..76507f0d7 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2873,8 +2873,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); // Add top-level secrets definition $secrets = []; foreach ($variables as $env) { + $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $env->key); $secrets[$env->key] = [ - 'file' => "{$this->secrets_dir}/{$env->key}", + 'file' => "{$this->secrets_dir}/{$safe_filename}", ]; } @@ -2904,7 +2905,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); // Update the compose file $composeFile['services'] = $services; - $composeFile['secrets'] = $secrets; + // merge with existing secrets if present + $existingSecrets = data_get($composeFile, 'secrets', []); + $composeFile['secrets'] = array_replace($existingSecrets, $secrets); $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); From 87967b8734760fb367449d8cc9d5ad4feba3af05 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:08:29 +0200 Subject: [PATCH 07/10] refactor(deployment): streamline Docker BuildKit detection and environment variable handling for enhanced security during application deployment --- app/Jobs/ApplicationDeploymentJob.php | 235 ++++++++++---------------- 1 file changed, 85 insertions(+), 150 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 76507f0d7..a5a971ae5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -171,8 +171,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private Collection|string $build_secrets; - private string $secrets_dir = ''; - public function tags() { // Do not remove this one, it needs to properly identify which worker is running the job @@ -279,8 +277,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); - // Check Docker Version - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); @@ -355,10 +352,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->write_deployment_configurations(); } - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { - $this->cleanup_build_secrets(); - } - $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -366,25 +359,34 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } - private function checkDockerVersion(): void + private function detectBuildKitCapabilities(): void { - // Use the build server if available, otherwise use the deployment server $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; + $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; try { - // Check Docker version (BuildKit requires Docker 18.09+) $dockerVersion = instant_remote_process( ["docker version --format '{{.Server.Version}}'"], $serverToCheck ); - // Parse version and check if >= 18.09 $versionParts = explode('.', $dockerVersion); $majorVersion = (int) $versionParts[0]; $minorVersion = (int) ($versionParts[1] ?? 0); - if ($majorVersion > 18 || ($majorVersion == 18 && $minorVersion >= 9)) { - // Test if BuildKit is available with secrets support + if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) { + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Using traditional build arguments."); + + return; + } + + $buildkitEnabled = instant_remote_process( + ["docker buildx version >/dev/null 2>&1 && echo 'available' || echo 'not-available'"], + $serverToCheck + ); + + if (trim($buildkitEnabled) !== 'available') { $buildkitTest = instant_remote_process( ["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], $serverToCheck @@ -392,18 +394,35 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (trim($buildkitTest) === 'supported') { $this->dockerBuildkitSupported = true; - $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; - $this->application_deployment_queue->addLogEntry("Docker BuildKit with secrets support detected on {$serverName}. Build secrets will be used for enhanced security."); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}."); + $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); } else { - $this->application_deployment_queue->addLogEntry('Docker BuildKit secrets not available. Falling back to build arguments.'); + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support enabled."); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); } } else { - $this->application_deployment_queue->addLogEntry("Docker version {$dockerVersion} detected. BuildKit requires 18.09+. Using build arguments."); + // Buildx is available, which means BuildKit is available + // Now specifically test for secrets support + $secretsTest = instant_remote_process( + ["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], + $serverToCheck + ); + + if (trim($secretsTest) === 'supported') { + $this->dockerBuildkitSupported = true; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}."); + $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + } else { + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported."); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + } } } catch (\Exception $e) { - // If check fails, default to false $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry('Could not determine Docker BuildKit support. Using build arguments as fallback.'); + $this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}"); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments as fallback.'); } } @@ -536,8 +555,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->generate_image_names(); $this->cleanup_git(); - // Check for BuildKit support and generate build secrets - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); $this->generate_build_env_variables(); $this->application->loadComposeFile(isInit: false); @@ -698,7 +716,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -1441,16 +1459,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Get user home directory $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); + + $env_flags = $this->generate_docker_env_flags_for_secrets(); + ray($env_flags); if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); } - $runCommand = "docker run -d --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { if ($this->dockerConfigFileExists === 'OK') { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } } $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); @@ -2629,12 +2650,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } if ($this->dockerBuildkitSupported) { - // Generate build secrets instead of build args $this->generate_build_secrets($variables); - // Ensure build_args is empty string when using secrets $this->build_args = ''; } else { - // Fallback to traditional build args $this->build_args = $variables->map(function ($value, $key) { $value = escapeshellarg($value); @@ -2643,57 +2661,45 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } } + private function generate_docker_env_flags_for_secrets() + { + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($variables->isEmpty()) { + return ''; + } + + return $variables + ->map(function ($env) { + $escaped_value = escapeshellarg($env->real_value); + + return "-e {$env->key}={$escaped_value}"; + }) + ->implode(' '); + } + private function generate_build_secrets(Collection $variables) { - $this->build_secrets = collect([]); - - // Only create secrets if there are variables to process if ($variables->isEmpty()) { $this->build_secrets = ''; return; } - $this->secrets_dir = "/tmp/.build_secrets_{$this->deployment_uuid}"; - - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "mkdir -p {$this->secrets_dir}" - ), 'hidden' => true]); - - // Generate a secret file for each environment variable - foreach ($variables as $key => $value) { - // keep id as-is, sanitize only filename - $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $key); - $secret_file_path = "{$this->secrets_dir}/{$safe_filename}"; - $escaped_value = base64_encode($value); - - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "echo '{$escaped_value}' | base64 -d > {$secret_file_path} && chmod 600 {$secret_file_path}" - ), 'hidden' => true]); - - $this->build_secrets->push("--secret id={$key},src={$secret_file_path}"); - } - - $this->build_secrets = $this->build_secrets->implode(' '); - } - - private function cleanup_build_secrets() - { - if ($this->dockerBuildkitSupported && $this->secrets_dir) { - // Clean up the secrets directory from the host - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "rm -rf {$this->secrets_dir}", - ), 'hidden' => true, 'ignore_errors' => true]); - } + $this->build_secrets = $variables + ->map(function ($value, $key) { + return "--secret id={$key},env={$key}"; + }) + ->implode(' '); } private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { - // When using BuildKit, we need to add the syntax directive and instructions on how to use secrets - $this->add_buildkit_secrets_to_dockerfile(); + // $this->add_buildkit_secrets_to_dockerfile(); } else { - // Traditional approach - add ARGs to the Dockerfile $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, @@ -2701,9 +2707,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - // Include ALL environment variables as build args (deprecating is_build_time flag) if ($this->pull_request_id === 0) { - // Get all environment variables except NIXPACKS_ prefixed ones $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { @@ -2731,58 +2735,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } } - private function add_buildkit_secrets_to_dockerfile() - { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), - 'hidden' => true, - 'save' => 'dockerfile', - ]); - $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - - // Check if BuildKit syntax is already present - $firstLine = $dockerfile->first(); - if (! str_starts_with($firstLine, '# syntax=')) { - // Add BuildKit syntax directive at the very beginning - $dockerfile->prepend('# syntax=docker/dockerfile:1'); - } - - // Create a comment block explaining how to use the secrets in RUN commands - $secretsComment = [ - '', - '# Build secrets are available. Use them in RUN commands like:', - '# For a single secret (inline environment variable):', - '# RUN --mount=type=secret,id=MY_SECRET MY_SECRET=$(cat /run/secrets/MY_SECRET) npm run build', - '', - '# For multiple secrets (inline environment variables):', - '# RUN --mount=type=secret,id=API_KEY --mount=type=secret,id=DB_URL \\', - '# API_KEY=$(cat /run/secrets/API_KEY) \\', - '# DB_URL=$(cat /run/secrets/DB_URL) \\', - '# npm run build', - '', - '# Note: Do NOT use export. Variables are set inline for the specific command only.', - '', - ]; - - // Find where to insert the comments (after FROM statement) - $fromIndex = $dockerfile->search(function ($line) { - return str_starts_with(trim(strtoupper($line)), 'FROM'); - }); - - if ($fromIndex !== false) { - // Insert comments after FROM statement - foreach (array_reverse($secretsComment) as $comment) { - $dockerfile->splice($fromIndex + 1, 0, [$comment]); - } - } - - $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), - 'hidden' => true, - ]); - } - private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) { // Only process if we have secrets to mount @@ -2810,36 +2762,25 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - // Find all RUN commands and add secret mounts to them $modified = false; $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { $trim = ltrim($line); - // Only handle shell-form RUN; skip JSON-form and already-mounted lines - if (str_starts_with($trim, 'RUN') && !preg_match('/^RUN\s*\[/i', $trim) && !str_contains($line, '--mount=type=secret')) { - // Build the mount flags for all secrets + if (str_contains($line, '--mount=type=secret')) { + return $line; + } + + if (str_starts_with($trim, 'RUN')) { $mounts = []; foreach ($variables as $env) { - $mounts[] = "--mount=type=secret,id={$env->key}"; + $mounts[] = "--mount=type=secret,id={$env->key},env={$env->key}"; } if (! empty($mounts)) { - // Build inline environment variable assignments (no export) - $envAssignments = []; - foreach ($variables as $env) { - $envAssignments[] = "{$env->key}=\$(cat /run/secrets/{$env->key})"; - } - - // Replace RUN with RUN with mounts and inline env vars $mountString = implode(' ', $mounts); - $envString = implode(' ', $envAssignments); + $originalCommand = trim(substr($trim, 3)); - // Extract the original command - $originalCommand = trim(substr($trim, 3)); // Remove 'RUN' - - // Create the new RUN command with mounts and inline environment variables - // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command - $line = "RUN {$mountString} {$envString} {$originalCommand}"; + $line = "RUN {$mountString} {$originalCommand}"; $modified = true; } } @@ -2870,28 +2811,21 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); return $composeFile; } - // Add top-level secrets definition $secrets = []; foreach ($variables as $env) { - $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $env->key); $secrets[$env->key] = [ - 'file' => "{$this->secrets_dir}/{$safe_filename}", + 'environment' => $env->key, ]; } - // Add build.secrets to services that have a build context $services = data_get($composeFile, 'services', []); foreach ($services as $serviceName => &$service) { - // Only add secrets if the service has a build context defined if (isset($service['build'])) { - // Handle both string and array build configurations if (is_string($service['build'])) { - // Convert string build to array format $service['build'] = [ 'context' => $service['build'], ]; } - // Add secrets to build configuration if (! isset($service['build']['secrets'])) { $service['build']['secrets'] = []; } @@ -2903,13 +2837,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } } - // Update the compose file $composeFile['services'] = $services; - // merge with existing secrets if present $existingSecrets = data_get($composeFile, 'secrets', []); + if ($existingSecrets instanceof \Illuminate\Support\Collection) { + $existingSecrets = $existingSecrets->toArray(); + } $composeFile['secrets'] = array_replace($existingSecrets, $secrets); - $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); + $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file (using environment variables).'); return $composeFile; } From c1bee32f0991cc4192f7451665b39e35790d6edc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:34:38 +0200 Subject: [PATCH 08/10] feat(deployment): introduce 'use_build_secrets' setting for enhanced security during Docker builds and update related logic in deployment process --- app/Jobs/ApplicationDeploymentJob.php | 47 ++++++++++++------- .../Shared/EnvironmentVariable/All.php | 4 ++ ..._build_secrets_to_application_settings.php | 28 +++++++++++ .../shared/environment-variable/all.blade.php | 37 ++++++++++----- 4 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a5a971ae5..cc2929f26 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -361,6 +361,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function detectBuildKitCapabilities(): void { + // If build secrets are not enabled, skip detection and use traditional args + if (! $this->application->settings->use_build_secrets) { + $this->dockerBuildkitSupported = false; + + return; + } + $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; @@ -376,7 +383,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) { $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Using traditional build arguments."); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Build secrets feature disabled."); return; } @@ -395,11 +402,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (trim($buildkitTest) === 'supported') { $this->dockerBuildkitSupported = true; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}."); - $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + $this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.'); } else { $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support enabled."); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support."); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.'); } } else { // Buildx is available, which means BuildKit is available @@ -412,17 +419,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (trim($secretsTest) === 'supported') { $this->dockerBuildkitSupported = true; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}."); - $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + $this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.'); } else { $this->dockerBuildkitSupported = false; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported."); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.'); } } } catch (\Exception $e) { $this->dockerBuildkitSupported = false; $this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}"); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments as fallback.'); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but detection failed. Using traditional build arguments.'); } } @@ -555,7 +562,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->generate_image_names(); $this->cleanup_git(); - $this->detectBuildKitCapabilities(); $this->generate_build_env_variables(); $this->application->loadComposeFile(isInit: false); @@ -566,7 +572,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // For raw compose, we cannot automatically add secrets configuration // User must define it manually in their docker-compose file - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) { $this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.'); } } else { @@ -588,8 +594,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return; } - // Add build secrets to compose file if BuildKit is supported - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + // Add build secrets to compose file if enabled and BuildKit is supported + if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) { $composeFile = $this->add_build_secrets_to_compose($composeFile); } @@ -716,7 +722,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); - $this->detectBuildKitCapabilities(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -2336,11 +2341,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - if ($this->dockerBuildkitSupported) { + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { // Modify the nixpacks Dockerfile to use build secrets $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; + } elseif ($this->dockerBuildkitSupported) { + // BuildKit without secrets + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } else { $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } @@ -2649,10 +2657,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $variables = collect([])->merge($this->env_args); } - if ($this->dockerBuildkitSupported) { + // Check if build secrets are enabled and BuildKit is supported + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { $this->generate_build_secrets($variables); $this->build_args = ''; } else { + // Fall back to traditional build args $this->build_args = $variables->map(function ($value, $key) { $value = escapeshellarg($value); @@ -2663,6 +2673,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function generate_docker_env_flags_for_secrets() { + // Only generate env flags if build secrets are enabled + if (! $this->application->settings->use_build_secrets) { + return ''; + } + $variables = $this->pull_request_id === 0 ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); @@ -2737,8 +2752,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) { - // Only process if we have secrets to mount - if (empty($this->build_secrets)) { + // Only process if build secrets are enabled and we have secrets to mount + if (! $this->application->settings->use_build_secrets || empty($this->build_secrets)) { return; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 9429c5f25..a71400f4c 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -25,6 +25,8 @@ class All extends Component public bool $is_env_sorting_enabled = false; + public bool $use_build_secrets = false; + protected $listeners = [ 'saveKey' => 'submit', 'refreshEnvs', @@ -34,6 +36,7 @@ class All extends Component public function mount() { $this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false); + $this->use_build_secrets = data_get($this->resource, 'settings.use_build_secrets', false); $this->resourceClass = get_class($this->resource); $resourceWithPreviews = [\App\Models\Application::class]; $simpleDockerfile = filled(data_get($this->resource, 'dockerfile')); @@ -49,6 +52,7 @@ class All extends Component $this->authorize('manageEnvironment', $this->resource); $this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled; + $this->resource->settings->use_build_secrets = $this->use_build_secrets; $this->resource->settings->save(); $this->getDevView(); $this->dispatch('success', 'Environment variable settings updated.'); diff --git a/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php b/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php new file mode 100644 index 000000000..b78f391fc --- /dev/null +++ b/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php @@ -0,0 +1,28 @@ +boolean('use_build_secrets')->default(false)->after('is_build_server_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('use_build_secrets'); + }); + } +}; diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 4518420dd..61e496d12 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -13,17 +13,32 @@ @endcan