Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/Actions/Application/CleanupDockerBuildCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Actions\Application;

use App\Models\Application;
use App\Models\Server;
use App\Services\DockerBuildCacheConfiguration;
use Lorisleiva\Actions\Concerns\AsAction;

final class CleanupDockerBuildCache
{
use AsAction;

public function handle(Application $application): void
{
$serverIds = $application->deployment_queue()
->whereNotNull('build_server_id')
->pluck('build_server_id')
->push(data_get($application, 'destination.server.id'))
->filter()
->unique();

if ($serverIds->isEmpty()) {
return;
}

$cachePath = DockerBuildCacheConfiguration::localCachePath($application->uuid);

Server::query()->whereIn('id', $serverIds)->each(function (Server $server) use ($cachePath): void {
instant_remote_process(
['rm -rf -- '.escapeshellarg($cachePath)],
$server,
false,
);
});
}
}
1 change: 1 addition & 0 deletions app/Actions/Server/CleanupDocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
$imagePruneCmd,
'docker builder prune -af',
"docker run --rm -v {$buildxMetadataVolume}:/root/.docker/buildx -v /var/run/docker.sock:/var/run/docker.sock {$helperImageWithVersion} docker buildx prune --builder coolify-railpack -af 2>/dev/null || true",
"docker run --rm -v {$buildxMetadataVolume}:/root/.docker/buildx -v /var/run/docker.sock:/var/run/docker.sock {$helperImageWithVersion} docker buildx prune --builder coolify-docker-cache -af 2>/dev/null || true",
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
"docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
"docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
Expand Down
14 changes: 14 additions & 0 deletions app/Http/Controllers/Api/ApplicationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class ApplicationsController extends Controller
'is_git_lfs_enabled',
'is_git_shallow_clone_enabled',
'disable_build_cache',
'docker_build_cache',
'preview_docker_build_cache',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'is_env_sorting_enabled',
Expand Down Expand Up @@ -373,6 +375,8 @@ public function applications(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down Expand Up @@ -566,6 +570,8 @@ public function create_public_application(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down Expand Up @@ -759,6 +765,8 @@ public function create_private_gh_app_application(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down Expand Up @@ -923,6 +931,8 @@ public function create_private_deploy_key_application(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down Expand Up @@ -1083,6 +1093,8 @@ public function create_dockerfile_application(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down Expand Up @@ -2679,6 +2691,8 @@ public function delete_by_uuid(Request $request)
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'External BuildKit cache for Dockerfile builds: enabled, cache_from {type, value}, cache_to {type, value}, and failure_policy (continue or fail).'],
'preview_docker_build_cache' => ['type' => 'object', 'nullable' => true, 'description' => 'Preview cache override using the same shape. Null inherits production; {enabled: false} disables it.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
Expand Down
80 changes: 77 additions & 3 deletions app/Jobs/ApplicationDeploymentJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Models\SwarmDocker;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Services\DockerBuildCacheConfiguration;
use App\Support\ValidationPatterns;
use App\Traits\EnvironmentVariableAnalyzer;
use App\Traits\ExecuteRemoteCommand;
Expand Down Expand Up @@ -197,6 +198,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue

private bool $dockerBuildxAvailable = false;

private ?DockerBuildCacheConfiguration $dockerBuildCacheConfiguration = null;

private bool $dockerSecretsSupported = false;

private bool $skip_build = false;
Expand Down Expand Up @@ -230,6 +233,13 @@ public function __construct(public int $application_deployment_queue_id)
if ($this->disableBuildCache) {
$this->force_rebuild = true;
}
if ($this->application->build_pack === 'dockerfile' && ! $this->disableBuildCache) {
$this->dockerBuildCacheConfiguration = DockerBuildCacheConfiguration::resolve(
production: $this->application->settings->docker_build_cache,
preview: $this->application->settings->preview_docker_build_cache,
isPreview: $this->pull_request_id !== 0,
);
}
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
Expand Down Expand Up @@ -2163,18 +2173,24 @@ private function prepare_builder_image(bool $firstTry = true)
$buildxMetadataVolume = isDev() && $this->server->isLocalhost()
? '-v coolify-buildx:/root/.docker/buildx'
: "-v {$this->serverUserHomeDir}/.docker/buildx:/root/.docker/buildx";
$dockerBuildCacheVolume = $this->docker_build_cache_volume();
if ($dockerBuildCacheVolume !== '') {
$localCachePath = DockerBuildCacheConfiguration::localCachePath($this->application->uuid);
instant_remote_process(['mkdir -p '.escapeshellarg($localCachePath)], $this->server);
$dockerBuildCacheVolume = ' '.$dockerBuildCacheVolume;
}
if ($this->use_build_server) {
if ($this->dockerConfigFileExists === 'NOK') {
throw new DeploymentException('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} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro {$buildxMetadataVolume} -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 {$buildxMetadataVolume}{$dockerBuildCacheVolume} -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
if ($this->dockerConfigFileExists === 'OK') {
$safeNetwork = escapeshellarg($this->destination->network);
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro {$buildxMetadataVolume} -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro {$buildxMetadataVolume}{$dockerBuildCacheVolume} -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$safeNetwork = escapeshellarg($this->destination->network);
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm {$buildxMetadataVolume} -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm {$buildxMetadataVolume}{$dockerBuildCacheVolume} -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
}
if ($firstTry) {
Expand Down Expand Up @@ -3641,8 +3657,63 @@ private function wrap_build_command_with_env_export(string $build_command): stri
return "cd {$this->workdir} && set -a && source ".self::BUILD_TIME_ENV_PATH." && set +a && {$build_command}";
}

private function docker_build_cache_command(string $buildCommand): string
{
if ($this->dockerBuildCacheConfiguration === null) {
return $buildCommand;
}

$cacheArguments = implode(' ', $this->dockerBuildCacheConfiguration->buildArguments($this->force_rebuild));
$buildxCommand = 'DOCKER_CONFIG=/root/.docker docker buildx build'
.' --builder '.DockerBuildCacheConfiguration::BUILDER_NAME
.' --load '.$cacheArguments.' ';

$cachedBuildCommand = preg_replace(
'/(?:DOCKER_BUILDKIT=1 )?docker build /',
$buildxCommand,
$buildCommand,
1,
);

if ($cachedBuildCommand === null || $cachedBuildCommand === $buildCommand) {
throw new DeploymentException('Unable to apply the configured external Docker build cache to the Dockerfile build command.');
}

$builderName = DockerBuildCacheConfiguration::BUILDER_NAME;
$prepareBuilder = '(DOCKER_CONFIG=/root/.docker docker buildx inspect '.$builderName.' >/dev/null 2>&1'
.' || DOCKER_CONFIG=/root/.docker docker buildx create --name '.$builderName.' --driver docker-container >/dev/null)';
$buildWithCache = $prepareBuilder.' && '.$cachedBuildCommand;

if ($this->dockerBuildCacheConfiguration->shouldFail()) {
return $buildWithCache;
}

return '('.$buildWithCache.') || { printf \'%s\\n\' \'External Docker build cache failed; retrying without it.\' >&2; '
.$buildCommand.'; }';
}

private function docker_build_cache_volume(): string
{
if ($this->dockerBuildCacheConfiguration?->usesLocalCache() !== true) {
return '';
}

$volume = DockerBuildCacheConfiguration::localCachePath($this->application->uuid).':/cache';

return '-v '.escapeshellarg($volume);
}

private function build_image()
{
if ($this->dockerBuildCacheConfiguration !== null && ! $this->dockerBuildxAvailable) {
if ($this->dockerBuildCacheConfiguration->shouldFail()) {
throw new DeploymentException('External Docker build cache requires the Docker buildx CLI plugin on the build server.');
}

$this->application_deployment_queue->addLogEntry('Warning: External Docker build cache was skipped because Docker buildx is unavailable.', 'stderr');
$this->dockerBuildCacheConfiguration = null;
}

// Add Coolify related variables to the build args/secrets
if (! $this->dockerSecretsSupported) {
// Traditional build args approach - generate COOLIFY_ variables locally
Expand Down Expand Up @@ -3758,6 +3829,7 @@ private function build_image()
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
}
}
$build_command = $this->docker_build_cache_command($build_command);
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
Expand Down Expand Up @@ -3843,6 +3915,7 @@ private function build_image()
$build_command = $this->wrap_build_command_with_env_export("docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
}
}
$build_command = $this->docker_build_cache_command($build_command);
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
Expand Down Expand Up @@ -3944,6 +4017,7 @@ private function build_image()
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
}
}
$build_command = $this->docker_build_cache_command($build_command);
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
Expand Down
Loading