diff --git a/app/Jobs/ApplicationDeployDockerImageJob.php b/app/Jobs/ApplicationDeployDockerImageJob.php new file mode 100644 index 000000000..dda5629c8 --- /dev/null +++ b/app/Jobs/ApplicationDeployDockerImageJob.php @@ -0,0 +1,111 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() + { + ray()->clearAll(); + ray('Deploying Docker Image'); + try { + $applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId); + $application = Application::find($applicationDeploymentQueue->application_id); + + $deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid'); + $dockerImage = data_get($application, 'docker_registry_image_name'); + $dockerImageTag = data_get($application, 'docker_registry_image_tag'); + $productionImageName = str("{$dockerImage}:{$dockerImageTag}"); + $destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first(); + $pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id'); + + $server = data_get($destination, 'server'); + $network = data_get($destination, 'network'); + + $containerName = generateApplicationContainerName($application, $pullRequestId); + savePrivateKeyToFs($server); + + ray("echo 'Starting deployment of {$productionImageName}.'"); + + $applicationDeploymentQueue->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: prepareHelperContainer($server, $network, $deploymentUuid) + ); + + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: generateComposeFile( + deploymentUuid: $deploymentUuid, + server: $server, + network: $network, + application: $application, + containerName: $containerName, + imageName: $productionImageName, + pullRequestId: $pullRequestId + ) + ); + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid) + ); + } catch (Throwable $e) { + $this->executeRemoteCommand( + server: $server, + logModel: $applicationDeploymentQueue, + commands: [ + "echo 'Oops something is not okay, are you okay? 😢'", + "echo '{$e->getMessage()}'", + "echo -n 'Deployment failed. Removing the new version of your application.'", + executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"), + ] + ); + // $this->next(ApplicationDeploymentStatus::FAILED->value); + throw $e; + } + } + // private function next(string $status) + // { + // // If the deployment is cancelled by the user, don't update the status + // if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + // $this->application_deployment_queue->update([ + // 'status' => $status, + // ]); + // } + // queue_next_deployment($this->application); + // if ($status === ApplicationDeploymentStatus::FINISHED->value) { + // $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + // } + // if ($status === ApplicationDeploymentStatus::FAILED->value) { + // $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); + // } + // } +} diff --git a/app/Jobs/ApplicationDeploySimpleDockerfileJob.php b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php new file mode 100644 index 000000000..a9e17bc80 --- /dev/null +++ b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php @@ -0,0 +1,29 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() { + ray('Deploying Simple Dockerfile'); + } +} diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php new file mode 100644 index 000000000..3216baa5a --- /dev/null +++ b/app/Jobs/ApplicationRestartJob.php @@ -0,0 +1,28 @@ +applicationDeploymentQueueId = $applicationDeploymentQueueId; + } + public function handle() { + ray('Restarting application'); + } +} diff --git a/app/Jobs/MultipleApplicationDeploymentJob.php b/app/Jobs/MultipleApplicationDeploymentJob.php new file mode 100644 index 000000000..32c98d3b0 --- /dev/null +++ b/app/Jobs/MultipleApplicationDeploymentJob.php @@ -0,0 +1,1165 @@ +clearScreen(); + $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); + $this->log_model = $this->application_deployment_queue; + $this->application = Application::find($this->application_deployment_queue->application_id); + $this->build_pack = data_get($this->application, 'build_pack'); + + $this->application_deployment_queue_id = $application_deployment_queue_id; + $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; + $this->commit = $this->application_deployment_queue->commit; + $this->force_rebuild = $this->application_deployment_queue->force_rebuild; + $this->restart_only = $this->application_deployment_queue->restart_only; + + $this->git_type = data_get($this->application_deployment_queue, 'git_type'); + + $source = data_get($this->application, 'source'); + if ($source) { + $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); + } + $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); + $this->server = $this->mainServer = $this->destination->server; + $this->serverUser = $this->server->user; + $this->basedir = generateBaseDir($this->deployment_uuid); + $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); + $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; + $this->is_debug_enabled = $this->application->settings->is_debug_enabled; + $this->saved_outputs = collect(); + $this->container_name = generateApplicationContainerName($this->application, 0); + } + + public function handle(): void + { + savePrivateKeyToFs($this->server); + $this->application_deployment_queue->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $this->addHosts = generateHostIpMapping($this->server, $this->destination->network); + + if ($this->application->dockerfile_target_build) { + $this->buildTarget = " --target {$this->application->dockerfile_target_build} "; + } + + // Check custom port + preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches); + if (count($matches) === 1) { + $this->customPort = $matches[0]; + $gitHost = str($this->application->git_repository)->before(':'); + $gitRepo = str($this->application->git_repository)->after('/'); + $this->customRepository = "$gitHost:$gitRepo"; + } else { + $this->customRepository = $this->application->git_repository; + } + try { + if ($this->application->isMultipleServerDeployment()) { + if ($this->application->build_pack === 'dockerimage') { + $this->dockerImage = $this->application->docker_registry_image_name; + $this->dockerImageTag = $this->application->docker_registry_image_tag; + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" + ], + ); + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + ray(prepareHelperContainer($this->server, $this->deployment_uuid)); + $this->execute_remote_command( + [prepareHelperContainer($this->server, $this->deployment_uuid)] + ); + } + } else { + throw new RuntimeException('Missing configuration for multiple server deployment.'); + } + // if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { + // $this->just_restart(); + // if ($this->server->isProxyShouldRun()) { + // dispatch(new ContainerStatusJob($this->server)); + // } + // $this->next(ApplicationDeploymentStatus::FINISHED->value); + // $this->application->isConfigurationChanged(true); + // return; + // } else if ($this->application->dockerfile) { + // $this->deploy_simple_dockerfile(); + // } else if ($this->application->build_pack === 'dockerimage') { + // $this->deploy_dockerimage_buildpack(); + // } else if ($this->application->build_pack === 'dockerfile') { + // $this->deploy_dockerfile_buildpack(); + // } else if ($this->application->build_pack === 'static') { + // $this->deploy_static_buildpack(); + // } else { + // $this->deploy_nixpacks_buildpack(); + // } + // if ($this->server->isProxyShouldRun()) { + // dispatch(new ContainerStatusJob($this->server)); + // } + // if ($this->application->docker_registry_image_name) { + // $this->push_to_docker_registry(); + // } + // $this->next(ApplicationDeploymentStatus::FINISHED->value); + // $this->application->isConfigurationChanged(true); + } catch (Exception $e) { + $this->fail($e); + throw $e; + } finally { + // if (isset($this->docker_compose_base64)) { + // $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + // $composeFileName = "$this->configuration_dir/docker-compose.yml"; + // $this->execute_remote_command( + // [ + // "mkdir -p $this->configuration_dir" + // ], + // [ + // "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName", + // ], + // [ + // "echo '{$readme}' > $this->configuration_dir/README.md", + // ] + // ); + // } + // $this->execute_remote_command( + // [ + // "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", + // "hidden" => true, + // "ignore_errors" => true, + // ] + // ); + // $this->execute_remote_command( + // [ + // "docker image prune -f >/dev/null 2>&1", + // "hidden" => true, + // "ignore_errors" => true, + // ] + // ); + } + } + private function push_to_docker_registry() + { + try { + instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true + ], + ); + if ($this->application->docker_registry_image_tag) { + // Tag image with latest + $this->execute_remote_command( + ['echo -n "Tagging and pushing image with latest tag."'], + [ + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + [ + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + ], + ); + } + $this->execute_remote_command([ + "echo -n 'Image pushed to docker registry.'" + ]); + } catch (Exception $e) { + $this->execute_remote_command( + ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"], + ); + ray($e); + } + } + // private function deploy_docker_compose() + // { + // $dockercompose_base64 = base64_encode($this->application->dockercompose); + // $this->execute_remote_command( + // [ + // "echo 'Starting deployment of {$this->application->name}.'" + // ], + // ); + // $this->prepare_builder_image(); + // $this->execute_remote_command( + // [ + // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") + // ], + // ); + // $this->build_image_name = Str::lower("{$this->customRepository}:build"); + // $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + // $this->save_environment_variables(); + // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); + // ray($containers); + // if ($containers->count() > 0) { + // foreach ($containers as $container) { + // $containerName = data_get($container, 'Names'); + // if ($containerName) { + // instant_remote_process( + // ["docker rm -f {$containerName}"], + // $this->application->destination->server + // ); + // } + // } + // } + + // $this->execute_remote_command( + // ["echo -n 'Starting services (could take a while)...'"], + // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], + // ); + // } + private function generate_image_names() + { + if ($this->application->dockerfile) { + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); + } + } else if ($this->application->build_pack === 'dockerimage') { + $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); + } else { + $this->dockerImageTag = str($this->commit)->substr(0, 128); + if ($this->application->docker_registry_image_name) { + $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}"); + } else { + $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}"); + } + } + } + private function just_restart() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.'); + } + private function check_image_locally_or_remotely() + { + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { + $this->execute_remote_command([ + "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true + ]); + $this->execute_remote_command([ + "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + ]); + } + } + // private function save_environment_variables() + // { + // $envs = collect([]); + // foreach ($this->application->environment_variables as $env) { + // $envs->push($env->key . '=' . $env->value); + // } + // $envs_base64 = base64_encode($envs->implode("\n")); + // $this->execute_remote_command( + // [ + // executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") + // ], + // ); + // } + private function deploy_simple_dockerfile() + { + $dockerfile_base64 = base64_encode($this->application->dockerfile); + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->application->name}.'" + ], + ); + $this->prepare_builder_image(); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location") + ], + ); + $this->generate_image_names(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->rolling_update(); + } + + private function deploy_dockerimage_buildpack() + { + // $this->dockerImage = $this->application->docker_registry_image_name; + // $this->dockerImageTag = $this->application->docker_registry_image_tag; + // ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); + // $this->execute_remote_command( + // [ + // "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" + // ], + // ); + // $this->generate_image_names(); + // $this->prepare_builder_image(); + $this->generate_compose_file(); + $this->rolling_update(); + } + + private function deploy_dockerfile_buildpack() + { + if (data_get($this->application, 'dockerfile_location')) { + $this->dockerfile_location = $this->application->dockerfile_location; + } + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->clone_repository(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->cleanup_git(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + // if ($this->application->additional_destinations) { + // $this->push_to_docker_registry(); + // $this->deploy_to_additional_destinations(); + // } else { + $this->rolling_update(); + // } + } + private function deploy_nixpacks_buildpack() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + if (!$this->force_rebuild) { + $this->check_image_locally_or_remotely(); + if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { + $this->execute_remote_command([ + "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'", + ]); + $this->generate_compose_file(); + $this->rolling_update(); + return; + } + if ($this->application->isConfigurationChanged()) { + $this->execute_remote_command([ + "echo 'Configuration changed. Rebuilding image.'", + ]); + } + } + $this->clone_repository(); + $this->cleanup_git(); + $this->generate_nixpacks_confs(); + $this->generate_compose_file(); + $this->generate_build_env_variables(); + $this->add_build_env_variables_to_dockerfile(); + $this->build_image(); + $this->rolling_update(); + } + private function deploy_static_buildpack() + { + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" + ], + ); + $this->prepare_builder_image(); + $this->check_git_if_build_needed(); + $this->set_base_dir(); + $this->generate_image_names(); + $this->clone_repository(); + $this->cleanup_git(); + $this->build_image(); + $this->generate_compose_file(); + $this->rolling_update(); + } + + private function rolling_update() + { + if (count($this->application->ports_mappings_array) > 0) { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], + ); + $this->stop_running_container(force: true); + $this->start_by_compose_file(); + } else { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Rolling update started.'"], + ); + $this->start_by_compose_file(); + $this->health_check(); + $this->stop_running_container(); + } + } + private function health_check() + { + if ($this->application->isHealthcheckDisabled()) { + $this->newVersionIsHealthy = true; + return; + } + // ray('New container name: ', $this->container_name); + if ($this->container_name) { + $counter = 1; + $this->execute_remote_command( + [ + "echo 'Waiting for healthcheck to pass on the new container.'" + ] + ); + if ($this->full_healthcheck_url) { + $this->execute_remote_command( + [ + "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" + ] + ); + } + while ($counter < $this->application->health_check_retries) { + $this->execute_remote_command( + [ + "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", + "hidden" => true, + "save" => "health_check" + ], + + ); + $this->execute_remote_command( + [ + "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" + ], + ); + if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { + $this->newVersionIsHealthy = true; + $this->application->update(['status' => 'running']); + $this->execute_remote_command( + [ + "echo 'New container is healthy.'" + ], + ); + break; + } + $counter++; + sleep($this->application->health_check_interval); + } + } + } + + private function prepare_builder_image() + { + $helperImage = config('coolify.helper_image'); + // 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); + + if ($this->dockerConfigFileExists === 'OK') { + $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --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}"; + } else { + $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + } + $this->execute_remote_command( + [ + "echo -n 'Preparing container with helper image: $helperImage.'", + ], + [ + $runCommand, + "hidden" => true, + ], + [ + "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") + ], + ); + } + private function deploy_to_additional_destinations() + { + $destination_ids = collect(str($this->application->additional_destinations)->explode(',')); + foreach ($destination_ids as $destination_id) { + $destination = StandaloneDocker::find($destination_id); + $server = $destination->server; + if ($server->team_id !== $this->mainServer->team_id) { + $this->execute_remote_command( + [ + "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'", + ], + ); + continue; + } + $this->server = $server; + $this->execute_remote_command( + [ + "echo -n 'Deploying to {$this->server->name}.'", + ], + ); + $this->prepare_builder_image(); + $this->generate_image_names(); + $this->rolling_update(); + } + } + private function set_base_dir() + { + $this->execute_remote_command( + [ + "echo -n 'Setting base directory to {$this->workdir}.'" + ], + ); + } + private function check_git_if_build_needed() + { + $this->generate_git_import_commands(); + $private_key = data_get($this->application, 'private_key.private_key'); + if ($private_key) { + $private_key = base64_encode($private_key); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh") + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa") + ], + [ + executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa") + ], + [ + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + "hidden" => true, + "save" => "git_commit_sha" + ], + ); + } else { + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"), + "hidden" => true, + "save" => "git_commit_sha" + ], + ); + } + + if ($this->saved_outputs->get('git_commit_sha')) { + $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + } + } + private function clone_repository() + { + $importCommands = $this->generate_git_import_commands(); + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + [ + "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" + ], + [ + $importCommands, "hidden" => true + ] + ); + } + + private function generate_git_import_commands() + { + $this->branch = $this->application->git_branch; + $commands = collect([]); + $git_clone_command = "git clone -q -b {$this->application->git_branch}"; + + if ($this->application->deploymentType() === 'source') { + $source_html_url = data_get($this->application, 'source.html_url'); + $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)); + $source_html_url_host = $url['host']; + $source_html_url_scheme = $url['scheme']; + + if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->is_public) { + $this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}"; + $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + } else { + $github_access_token = generate_github_installation_token($this->source); + $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}")); + $this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git"; + } + return $commands->implode(' && '); + } + } + if ($this->application->deploymentType() === 'deploy_key') { + $this->fullRepoUrl = $this->customRepository; + $private_key = data_get($this->application, 'private_key.private_key'); + if (is_null($private_key)) { + throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); + } + $private_key = base64_encode($private_key); + $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command_base); + $commands = collect([ + executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), + executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"), + executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"), + ]); + + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + return $commands->implode(' && '); + } + if ($this->application->deploymentType() === 'other') { + $this->fullRepoUrl = $this->customRepository; + $git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command); + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); + return $commands->implode(' && '); + } + } + + private function set_git_import_settings($git_clone_command) + { + if ($this->application->git_commit_sha !== 'HEAD') { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1"; + } + if ($this->application->settings->is_git_submodules_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive"; + } + if ($this->application->settings->is_git_lfs_enabled) { + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull"; + } + return $git_clone_command; + } + + private function cleanup_git() + { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "rm -fr {$this->basedir}/.git")], + ); + } + + private function generate_nixpacks_confs() + { + $nixpacks_command = $this->nixpacks_build_cmd(); + $this->execute_remote_command( + [ + "echo -n 'Generating nixpacks configuration with: $nixpacks_command'", + ], + [executeInDocker($this->deployment_uuid, $nixpacks_command)], + [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], + [executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")] + ); + } + + private function nixpacks_build_cmd() + { + $this->generate_env_variables(); + $nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start"; + if ($this->application->build_command) { + $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; + } + if ($this->application->start_command) { + $nixpacks_command .= " --start-cmd \"{$this->application->start_command}\""; + } + if ($this->application->install_command) { + $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; + } + $nixpacks_command .= " {$this->workdir}"; + return $nixpacks_command; + } + + private function generate_env_variables() + { + $this->env_args = collect([]); + foreach ($this->application->nixpacks_environment_variables_preview as $env) { + $this->env_args->push("--env {$env->key}={$env->value}"); + } + $this->env_args = $this->env_args->implode(' '); + } + + private function generate_compose_file() + { + $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables($ports); + + if (data_get($this->application, 'custom_labels')) { + $labels = collect(str($this->application->custom_labels)->explode(',')); + $labels = $labels->filter(function ($value, $key) { + return !Str::startsWith($value, 'coolify.'); + }); + $this->application->custom_labels = $labels->implode(','); + $this->application->save(); + } else { + $labels = collect(generateLabelsApplication($this->application, $this->preview)); + } + + $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, 0))->toArray(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $this->container_name => [ + 'image' => $this->production_image_name, + 'container_name' => $this->container_name, + 'restart' => RESTART_MODE, + 'environment' => $environment_variables, + 'labels' => $labels, + 'expose' => $ports, + 'networks' => [ + $this->destination->network, + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ], + 'mem_limit' => $this->application->limits_memory, + 'memswap_limit' => $this->application->limits_memory_swap, + 'mem_swappiness' => $this->application->limits_memory_swappiness, + 'mem_reservation' => $this->application->limits_memory_reservation, + 'cpus' => (int) $this->application->limits_cpus, + 'cpuset' => $this->application->limits_cpuset, + 'cpu_shares' => $this->application->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->destination->network => [ + 'external' => true, + 'name' => $this->destination->network, + 'attachable' => true + ] + ] + ]; + if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { + $docker_compose['services'][$this->container_name]['logging'] = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => "tcp://127.0.0.1:24224", + 'fluentd-async' => "true", + 'fluentd-sub-second-precision' => "true", + ] + ]; + } + if ($this->application->settings->is_gpu_enabled) { + ray('asd'); + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($this->application, 'settings.gpu_options', []) + ] + ]; + if (data_get($this->application, 'settings.gpu_count')) { + $count = data_get($this->application, 'settings.gpu_count'); + if ($count === 'all') { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($this->application, 'settings.gpu_device_ids')) { + $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids'); + } + } + if ($this->application->isHealthcheckDisabled()) { + data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + // if ($this->build_pack === 'dockerfile') { + // $docker_compose['services'][$this->container_name]['build'] = [ + // 'context' => $this->workdir, + // 'dockerfile' => $this->workdir . $this->dockerfile_location, + // ]; + // } + $this->docker_compose = Yaml::dump($docker_compose, 10); + $this->docker_compose_base64 = base64_encode($this->docker_compose); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->application->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables($ports) + { + $environment_variables = collect(); + foreach ($this->application->runtime_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($this->application->nixpacks_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + // Add PORT if not exists, use the first port as default + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { + $environment_variables->push("PORT={$ports[0]}"); + } + return $environment_variables->all(); + } + + private function generate_healthcheck_commands() + { + if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { + // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. + return 'exit 0'; + } + if (!$this->application->health_check_port) { + $health_check_port = $this->application->ports_exposes_array[0]; + } else { + $health_check_port = $this->application->health_check_port; + } + if ($this->application->health_check_path) { + $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" + ]; + } else { + $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"; + $generated_healthchecks_commands = [ + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" + ]; + } + return implode(' ', $generated_healthchecks_commands); + } + private function pull_latest_image($image) + { + $this->execute_remote_command( + ["echo -n 'Pulling latest image ($image) from the registry.'"], + + [ + executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true + ] + ); + } + private function build_image() + { + if ($this->application->build_pack === 'static') { + $this->execute_remote_command([ + "echo -n 'Static deployment. Copying static assets to the image.'", + ]); + } else { + $this->execute_remote_command( + [ + "echo -n 'Building docker image started.'", + ], + ["echo -n 'To check the current progress, click on Show Debug Logs.'"] + ); + } + + if ($this->application->settings->is_static || $this->application->build_pack === 'static') { + if ($this->application->static_image) { + $this->pull_latest_image($this->application->static_image); + } + 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 +COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); + $nginx_config = base64_encode("server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + }"); + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "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"); + + $nginx_config = base64_encode("server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + }"); + } + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile") + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") + ], + [ + executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + ] + ); + } else { + // Pure Dockerfile based deployment + if ($this->application->dockerfile) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "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}"), "hidden" => true + ]); + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "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}"), "hidden" => true + ]); + } + } + $this->execute_remote_command([ + "echo -n 'Building docker image completed.'", + ]); + } + + private function stop_running_container(bool $force = false) + { + $this->execute_remote_command(["echo -n 'Removing old container.'"]); + if ($this->newVersionIsHealthy || $force) { + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, 0); + $containers = $containers->filter(function ($container) { + return data_get($container, 'Names') !== $this->container_name; + }); + $containers->each(function ($container) { + $containerName = data_get($container, 'Names'); + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + }); + $this->execute_remote_command( + [ + "echo 'Rolling update completed.'" + ], + ); + } else { + $this->execute_remote_command( + ["echo -n 'New container is not healthy, rolling back to the old container.'"], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + } + } + + private function start_by_compose_file() + { + if ($this->application->build_pack === 'dockerimage') { + $this->execute_remote_command( + ["echo -n 'Pulling latest images from the registry.'"], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + ); + } else { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + ); + } + } + + private function generate_build_env_variables() + { + $this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]); + foreach ($this->application->build_environment_variables_preview as $env) { + $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\""); + } + $this->build_args = $this->build_args->implode(' '); + } + + private function add_build_env_variables_to_dockerfile() + { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' + ]); + $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + foreach ($this->application->build_environment_variables as $env) { + $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"), + "hidden" => true + ]); + } + + private function next(string $status) + { + // If the deployment is cancelled by the user, don't update the status + if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + $this->application_deployment_queue->update([ + 'status' => $status, + ]); + } + queue_next_deployment($this->application); + if ($status === ApplicationDeploymentStatus::FINISHED->value) { + $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + } + if ($status === ApplicationDeploymentStatus::FAILED->value) { + $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); + } + } + + public function failed(Throwable $exception): void + { + $this->execute_remote_command( + ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], + ["echo '{$exception->getMessage()}'", 'type' => 'err'], + ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] + ); + + $this->next(ApplicationDeploymentStatus::FAILED->value); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index f049351e2..785ef3040 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -332,4 +332,14 @@ class Application extends BaseModel return true; } } + public function isMultipleServerDeployment() + { + if (isDev()) { + return true; + } + if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) { + return true; + } + return false; + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 0d57cea55..30e07c1d8 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -171,7 +171,7 @@ class Server extends BaseModel break; } $result = $this->validateConnection(); - ray('validateConnection: ' . $result); + // ray('validateConnection: ' . $result); if (!$result) { $serverUptimeCheckNumber++; $this->update([ diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0a53c5bf6..1306f645c 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -12,7 +12,6 @@ use Illuminate\Support\Str; trait ExecuteRemoteCommand { public ?string $save = null; - public function execute_remote_command(...$commands) { static::$batch_counter++; diff --git a/app/Traits/ExecuteRemoteCommandNew.php b/app/Traits/ExecuteRemoteCommandNew.php new file mode 100644 index 000000000..ca56d9e50 --- /dev/null +++ b/app/Traits/ExecuteRemoteCommandNew.php @@ -0,0 +1,77 @@ +each(function ($singleCommand) use ($server, $logModel) { + $command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null; + if ($command === null) { + throw new \RuntimeException('Command is not set'); + } + $hidden = data_get($singleCommand, 'hidden', false); + $customType = data_get($singleCommand, 'type'); + $ignoreErrors = data_get($singleCommand, 'ignore_errors', false); + $save = data_get($singleCommand, 'save'); + + $remote_command = generateSshCommand($server, $command); + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) { + $output = str($output)->trim(); + if ($output->startsWith('╔')) { + $output = "\n" . $output; + } + $newLogEntry = [ + 'command' => remove_iip($command), + 'output' => remove_iip($output), + 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', + 'timestamp' => Carbon::now('UTC'), + 'hidden' => $hidden, + 'batch' => static::$batch_counter, + ]; + + if (!$logModel->logs) { + $newLogEntry['order'] = 1; + } else { + $previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $newLogEntry['order'] = count($previousLogs) + 1; + } + + $previousLogs[] = $newLogEntry; + $logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR); + $logModel->save(); + + if ($save) { + $this->remoteCommandOutputs[$save] = str($output)->trim(); + } + }); + $logModel->update([ + 'current_process_id' => $process->id(), + ]); + + $processResult = $process->wait(); + if ($processResult->exitCode() !== 0) { + if (!$ignoreErrors) { + $status = ApplicationDeploymentStatus::FAILED->value; + $logModel->status = $status; + $logModel->save(); + throw new \RuntimeException($processResult->errorOutput()); + } + } + }); + } +} diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index fce225dc5..1cead8a54 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -1,8 +1,15 @@ count() > 0) { return; } + // New deployment + // dispatchDeploymentJob($deployment->id); dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, ))->onConnection('long-running')->onQueue('long-running'); + } function queue_next_deployment(Application $application) { $next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first(); if ($next_found) { + // New deployment + // dispatchDeploymentJob($next_found->id); dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $next_found->id, ))->onConnection('long-running')->onQueue('long-running'); + + } +} +function dispatchDeploymentJob($id) +{ + $applicationQueue = ApplicationDeploymentQueue::find($id); + $application = Application::find($applicationQueue->application_id); + + $isRestartOnly = data_get($applicationQueue, 'restart_only'); + $isSimpleDockerFile = data_get($application, 'dockerfile'); + $isDockerImage = data_get($application, 'build_pack') === 'dockerimage'; + + if ($isRestartOnly) { + ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else if ($isSimpleDockerFile) { + ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else if ($isDockerImage) { + ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running'); + } else { + throw new Exception('Unknown build pack'); + } +} + +// Deployment things +function generateHostIpMapping(Server $server, string $network) +{ + // Generate custom host<->ip hostnames + $allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server); + $allContainers = format_docker_command_output_to_json($allContainers); + $ips = collect([]); + if (count($allContainers) > 0) { + $allContainers = $allContainers[0]; + foreach ($allContainers as $container) { + $containerName = data_get($container, 'Name'); + if ($containerName === 'coolify-proxy') { + continue; + } + $containerIp = data_get($container, 'IPv4Address'); + if ($containerName && $containerIp) { + $containerIp = str($containerIp)->before('/'); + $ips->put($containerName, $containerIp->value()); + } + } + } + return $ips->map(function ($ip, $name) { + return "--add-host $name:$ip"; + })->implode(' '); +} + +function generateBaseDir(string $deplyomentUuid) +{ + return "/artifacts/$deplyomentUuid"; +} +function generateWorkdir(string $deplyomentUuid, Application $application) +{ + return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/'); +} + +function prepareHelperContainer(Server $server, string $network, string $deploymentUuid) +{ + $basedir = generateBaseDir($deploymentUuid); + $helperImage = config('coolify.helper_image'); + + $serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server); + $dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server); + + $commands = collect([]); + if ($dockerConfigFileExists === 'OK') { + $commands->push([ + "command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage", + "hidden" => true, + ]); + } else { + $commands->push([ + "command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}", + "hidden" => true, + ]); + } + $commands->push([ + "command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"), + "hidden" => true, + ]); + return $commands; +} + +function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0) +{ + $ports = $application->settings->is_static ? [80] : $application->ports_exposes_array; + $workDir = generateWorkdir($deploymentUuid, $application); + $persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId); + $volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId); + $environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId); + + if (data_get($application, 'custom_labels')) { + $labels = collect(str($application->custom_labels)->explode(',')); + $labels = $labels->filter(function ($value, $key) { + return !str($value)->startsWith('coolify.'); + }); + $application->custom_labels = $labels->implode(','); + $application->save(); + } else { + $labels = collect(generateLabelsApplication($application, $preview)); + } + if ($pullRequestId !== 0) { + $labels = collect(generateLabelsApplication($application, $preview)); + } + $labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $containerName => [ + 'image' => $imageName, + 'container_name' => $containerName, + 'restart' => RESTART_MODE, + 'environment' => $environment_variables, + 'labels' => $labels, + 'expose' => $ports, + 'networks' => [ + $network, + ], + 'mem_limit' => $application->limits_memory, + 'memswap_limit' => $application->limits_memory_swap, + 'mem_swappiness' => $application->limits_memory_swappiness, + 'mem_reservation' => $application->limits_memory_reservation, + 'cpus' => (int) $application->limits_cpus, + 'cpuset' => $application->limits_cpuset, + 'cpu_shares' => $application->limits_cpu_shares, + ] + ], + 'networks' => [ + $network => [ + 'external' => true, + 'name' => $network, + 'attachable' => true + ] + ] + ]; + if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) { + $docker_compose['services'][$containerName]['logging'] = [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => "tcp://127.0.0.1:24224", + 'fluentd-async' => "true", + 'fluentd-sub-second-precision' => "true", + ] + ]; + } + if ($application->settings->is_gpu_enabled) { + ray('asd'); + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [ + [ + 'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'), + 'capabilities' => ['gpu'], + 'options' => data_get($application, 'settings.gpu_options', []) + ] + ]; + if (data_get($application, 'settings.gpu_count')) { + $count = data_get($application, 'settings.gpu_count'); + if ($count === 'all') { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count; + } else { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; + } + } else if (data_get($application, 'settings.gpu_device_ids')) { + $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids'); + } + } + if ($application->isHealthcheckDisabled()) { + data_forget($docker_compose, 'services.' . $containerName . '.healthcheck'); + } + if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) { + $docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$containerName]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $commands = collect([]); + $commands->push([ + "command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"), + "hidden" => true, + ]); + return $commands; +} +function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0) +{ + $local_persistent_volumes = []; + foreach ($application->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + if ($pullRequestId !== 0) { + $volume_name = $volume_name . '-pr-' . $pullRequestId; + } + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; +} + +function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0) +{ + $local_persistent_volumes_names = []; + foreach ($application->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + + if ($pullRequestId !== 0) { + $name = $name . '-pr-' . $pullRequestId; + } + + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; +} +function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0) +{ + $environment_variables = collect(); + // ray('Generate Environment Variables')->green(); + if ($pullRequestId === 0) { + // ray($this->application->runtime_environment_variables)->green(); + foreach ($application->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($application->nixpacks_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } else { + // ray($this->application->runtime_environment_variables_preview)->green(); + foreach ($application->runtime_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + foreach ($application->nixpacks_environment_variables_preview as $env) { + $environment_variables->push("$env->key=$env->value"); + } + } + // Add PORT if not exists, use the first port as default + if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) { + $environment_variables->push("PORT={$ports[0]}"); + } + return $environment_variables->all(); +} + +function rollingUpdate(Application $application, string $deploymentUuid) +{ + $commands = collect([]); + $workDir = generateWorkdir($deploymentUuid, $application); + if (count($application->ports_mappings_array) > 0) { + // $this->execute_remote_command( + // [ + // "echo '\n----------------------------------------'", + // ], + // ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], + // ); + // $this->stop_running_container(force: true); + // $this->start_by_compose_file(); + } else { + $commands->push( + [ + "command" => "echo '\n----------------------------------------'" + ], + [ + "command" => "echo -n 'Rolling update started.'" + ] + ); + if ($application->build_pack === 'dockerimage') { + $commands->push( + ["echo -n 'Pulling latest images from the registry.'"], + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true], + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true], + ); + } else { + $commands->push( + [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true], + ); + } + return $commands; } }