merge: merge next branch into feat-traefik-version-checker

Merged latest changes from the next branch to keep the feature branch
up to date. No conflicts were encountered during the merge.

Changes from next branch:
- Updated application deployment job error logging
- Updated server manager job and instance settings
- Removed PullHelperImageJob in favor of updated approach
- Database migration refinements
- Updated versions.json with latest component versions

All automatic merges were successful and no manual conflict resolution
was required.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude committed 2025-11-17 14:56:24 +01:00
commit 1270136da9
11 files changed
+538 -267

No files matched your search

+3 -2
View File
@@ -2,7 +2,6 @@
namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -50,7 +49,9 @@ class UpdateCoolify
private function update()
{
PullHelperImageJob::dispatch($this->server);
$helperImage = config('constants.coolify.helper_image');
$latest_version = getHelperVersion();
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
instant_remote_process(["docker pull -q $image"], $this->server, false);
+184 -138
View File
@@ -976,7 +976,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry('Failed to push image to docker registry. Please check debug logs for more information.');
if ($forceFail) {
throw new DeploymentException($e->getMessage(), 69420);
throw new DeploymentException(get_class($e).': '.$e->getMessage(), $e->getCode(), $e);
}
}
}
@@ -1610,123 +1610,131 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function rolling_update()
{
$this->checkForCancellation();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry('Rolling update started.');
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
],
);
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
} else {
if ($this->use_build_server) {
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
}
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
}
if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
}
if ($this->pull_request_id !== 0) {
$this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry('Pull request deployment, rolling update is not supported.');
}
if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('Custom IP address is set, rolling update is not supported.');
}
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
try {
$this->checkForCancellation();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry('Rolling update started.');
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
],
);
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
} else {
if ($this->use_build_server) {
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
}
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
}
if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
}
if ($this->pull_request_id !== 0) {
$this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry('Pull request deployment, rolling update is not supported.');
}
if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('Custom IP address is set, rolling update is not supported.');
}
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
$this->application_deployment_queue->addLogEntry('Rolling update started.');
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
}
}
} catch (Exception $e) {
throw new DeploymentException('Rolling update failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e);
}
}
private function health_check()
{
if ($this->server->isSwarm()) {
// Implement healthcheck for swarm
} else {
if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) {
$this->newVersionIsHealthy = true;
try {
if ($this->server->isSwarm()) {
// Implement healthcheck for swarm
} else {
if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) {
$this->newVersionIsHealthy = true;
return;
}
if ($this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
}
if ($this->container_name) {
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
return;
}
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
$sleeptime = 0;
while ($sleeptime < $this->application->health_check_start_period) {
Sleep::for(1)->seconds();
$sleeptime++;
if ($this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
}
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',
'append' => false,
],
[
"docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
'hidden' => true,
'save' => 'health_check_logs',
'append' => false,
],
);
$this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
$health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
if (empty($health_check_logs)) {
$health_check_logs = '(no logs)';
if ($this->container_name) {
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
$health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
$this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->application_deployment_queue->addLogEntry('New container is healthy.');
break;
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
$this->newVersionIsHealthy = false;
$this->query_logs();
break;
}
$counter++;
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
$sleeptime = 0;
while ($sleeptime < $this->application->health_check_interval) {
while ($sleeptime < $this->application->health_check_start_period) {
Sleep::for(1)->seconds();
$sleeptime++;
}
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
$this->query_logs();
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',
'append' => false,
],
[
"docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
'hidden' => true,
'save' => 'health_check_logs',
'append' => false,
],
);
$this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
$health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
if (empty($health_check_logs)) {
$health_check_logs = '(no logs)';
}
$health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
$this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->application_deployment_queue->addLogEntry('New container is healthy.');
break;
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
$this->newVersionIsHealthy = false;
$this->query_logs();
break;
}
$counter++;
$sleeptime = 0;
while ($sleeptime < $this->application->health_check_interval) {
Sleep::for(1)->seconds();
$sleeptime++;
}
}
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
$this->query_logs();
}
}
}
} catch (Exception $e) {
throw new DeploymentException('Health check failed ('.get_class($e).'): '.$e->getMessage(), $e->getCode(), $e);
}
}
@@ -3034,58 +3042,66 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function stop_running_container(bool $force = false)
{
$this->application_deployment_queue->addLogEntry('Removing old containers.');
if ($this->newVersionIsHealthy || $force) {
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->graceful_shutdown_container($this->container_name);
} else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id);
try {
$this->application_deployment_queue->addLogEntry('Removing old containers.');
if ($this->newVersionIsHealthy || $force) {
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->graceful_shutdown_container($this->container_name);
} else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id);
});
}
$containers->each(function ($container) {
$this->graceful_shutdown_container(data_get($container, 'Names'));
});
}
$containers->each(function ($container) {
$this->graceful_shutdown_container(data_get($container, 'Names'));
});
} else {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
$this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI.");
$this->application_deployment_queue->addLogEntry('----------------------------------------');
}
$this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
$this->failDeployment();
$this->graceful_shutdown_container($this->container_name);
}
} else {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
$this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI.");
$this->application_deployment_queue->addLogEntry('----------------------------------------');
}
$this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
$this->failDeployment();
$this->graceful_shutdown_container($this->container_name);
} catch (Exception $e) {
throw new DeploymentException("Failed to stop running container: {$e->getMessage()}", $e->getCode(), $e);
}
}
private function start_by_compose_file()
{
// Ensure .env file exists before docker compose tries to load it (defensive programming)
$this->execute_remote_command(
["touch {$this->configuration_dir}/.env", 'hidden' => true],
);
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
try {
// Ensure .env file exists before docker compose tries to load it (defensive programming)
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
["touch {$this->configuration_dir}/.env", 'hidden' => true],
);
} else {
if ($this->use_build_server) {
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
);
if ($this->use_build_server) {
$this->execute_remote_command(
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
);
}
}
$this->application_deployment_queue->addLogEntry('New container started.');
} catch (Exception $e) {
throw new DeploymentException("Failed to start container: {$e->getMessage()}", $e->getCode(), $e);
}
$this->application_deployment_queue->addLogEntry('New container started.');
}
private function analyzeBuildTimeVariables($variables)
@@ -3829,7 +3845,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
* Fail the deployment.
* Sends failure notification and queues next deployment.
*/
private function failDeployment(): void
protected function failDeployment(): void
{
$this->transitionToStatus(ApplicationDeploymentStatus::FAILED);
}
@@ -3837,8 +3853,38 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->failDeployment();
// Log comprehensive error information
$errorMessage = $exception->getMessage() ?: 'Unknown error occurred';
$errorCode = $exception->getCode();
$errorClass = get_class($exception);
$this->application_deployment_queue->addLogEntry('========================================', 'stderr');
$this->application_deployment_queue->addLogEntry("Deployment failed: {$errorMessage}", 'stderr');
$this->application_deployment_queue->addLogEntry("Error type: {$errorClass}", 'stderr', hidden: true);
$this->application_deployment_queue->addLogEntry("Error code: {$errorCode}", 'stderr', hidden: true);
// Log the exception file and line for debugging
$this->application_deployment_queue->addLogEntry("Location: {$exception->getFile()}:{$exception->getLine()}", 'stderr', hidden: true);
// Log previous exceptions if they exist (for chained exceptions)
$previous = $exception->getPrevious();
if ($previous) {
$this->application_deployment_queue->addLogEntry('Caused by:', 'stderr', hidden: true);
$previousMessage = $previous->getMessage() ?: 'No message';
$previousClass = get_class($previous);
$this->application_deployment_queue->addLogEntry(" {$previousClass}: {$previousMessage}", 'stderr', hidden: true);
$this->application_deployment_queue->addLogEntry(" at {$previous->getFile()}:{$previous->getLine()}", 'stderr', hidden: true);
}
// Log first few lines of stack trace for debugging
$trace = $exception->getTraceAsString();
$traceLines = explode("\n", $trace);
$this->application_deployment_queue->addLogEntry('Stack trace (first 5 lines):', 'stderr', hidden: true);
foreach (array_slice($traceLines, 0, 5) as $traceLine) {
$this->application_deployment_queue->addLogEntry(" {$traceLine}", 'stderr', hidden: true);
}
$this->application_deployment_queue->addLogEntry('========================================', 'stderr');
if ($this->application->build_pack !== 'dockercompose') {
$code = $exception->getCode();
-30
View File
@@ -1,30 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct(public Server $server)
{
$this->onQueue('high');
}
public function handle(): void
{
$helperImage = config('constants.coolify.helper_image');
$latest_version = getHelperVersion();
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ class ServerManagerJob implements ShouldQueue
Log::channel('scheduled-errors')->error('Failed to dispatch ServerConnectionCheck', [
'server_id' => $server->id,
'server_name' => $server->name,
'error' => $e->getMessage(),
'error' => get_class($e).': '.$e->getMessage(),
]);
}
});
@@ -103,7 +103,7 @@ class ServerManagerJob implements ShouldQueue
Log::channel('scheduled-errors')->error('Error processing server tasks', [
'server_id' => $server->id,
'server_name' => $server->name,
'error' => $e->getMessage(),
'error' => get_class($e).': '.$e->getMessage(),
]);
}
}
-9
View File
@@ -2,7 +2,6 @@
namespace App\Models;
use App\Jobs\PullHelperImageJob;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Spatie\Url\Url;
@@ -35,14 +34,6 @@ class InstanceSettings extends Model
protected static function booted(): void
{
static::updated(function ($settings) {
if ($settings->wasChanged('helper_version')) {
Server::chunkById(100, function ($servers) {
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
});
}
// Clear trusted hosts cache when FQDN changes
if ($settings->wasChanged('fqdn')) {
\Cache::forget('instance_settings_fqdn_host');