mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-14 16:52:45 -04:00
Sentry self-hosted (getsentry/self-hosted 26.8.0) could not be deployed as a Docker Compose application. Three separate problems blocked it: - YAML collection aliases: the first fix raised the limit to 256 only in Application::oldRawParser(). The safety check added in v4.4.0 (validateDockerComposeForInjection) and all other Compose parse calls still used the Symfony default of 128, so the Sentry file (133 aliases) failed with "Maximum number of collection aliases (128) exceeded". Add parseDockerComposeYaml() and use it for every Compose parse. - Argument list too long: the deployment job sent the generated Compose file as base64 inside the SSH command. Linux limits one argument to 128 KiB, and the Sentry file is about 370 KB. execute_remote_command() now accepts an 'input' key that streams content over SSH stdin, and the job writes the Compose file with it. - A list-style build arg without a value (build.args: [SENTRY_IMAGE]) merged into a map environment kept its numeric key, so Docker Compose rejected the file with "non-string key in ...environment: 0". It now becomes NAME with no value, as in Compose. Closes #11725
130 lines
5.7 KiB
PHP
130 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Actions\Service;
|
|
|
|
use App\Actions\Shared\EnsureContentFilesOnServer;
|
|
use App\Models\LocalFileVolume;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceApplication;
|
|
use App\Models\ServiceDatabase;
|
|
use Illuminate\Support\Collection;
|
|
use Lorisleiva\Actions\Concerns\AsAction;
|
|
use Lorisleiva\Actions\Decorators\JobDecorator;
|
|
|
|
class StartService
|
|
{
|
|
use AsAction;
|
|
|
|
public function configureJob(JobDecorator $job): void
|
|
{
|
|
$job->onQueue(deployment_queue());
|
|
}
|
|
|
|
public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
|
|
{
|
|
$service->parse();
|
|
// Fetch remote secrets before stopping: if the secret manager fails, the service keeps running.
|
|
// saveComposeConfigs() below reuses the fetched secrets.
|
|
$service->ensureRemoteSecretsResolvable($service->environment_variables()->get());
|
|
if ($this->shouldStopBeforeStarting($pullLatestImages, $stopBeforeStart)) {
|
|
StopService::run(service: $service, dockerCleanup: false);
|
|
}
|
|
$service->saveComposeConfigs();
|
|
$service->isConfigurationChanged(save: true);
|
|
$service->applications()->get()->each->resetRestartLimit();
|
|
$workdir = $service->workdir();
|
|
// $commands[] = "cd {$workdir}";
|
|
$commands[] = "echo 'Saved configuration files to {$workdir}.'";
|
|
// Ensure .env exists in the correct directory before docker compose tries to load it
|
|
// This is defensive programming - saveComposeConfigs() already creates it,
|
|
// but we guarantee it here in case of any edge cases or manual deployments
|
|
$commands[] = "touch {$workdir}/.env";
|
|
$commands = array_merge($commands, EnsureContentFilesOnServer::echoCommands($this->contentFileStorages($service), $service->server));
|
|
$commands = array_merge($commands, self::composeVolumeWarningCommands($service));
|
|
// The script reaches the server on stdin. Compose prompts (for example "Volume ... exists but
|
|
// doesn't match configuration. Recreate?") would read the next script lines as the answer.
|
|
// Give Compose no stdin so it uses the default answer (keep the volume). Never pass --yes:
|
|
// it recreates the volume and deletes its data.
|
|
if ($pullLatestImages) {
|
|
$commands[] = "echo 'Pulling images.'";
|
|
$commands[] = "docker compose --project-directory {$workdir} pull < /dev/null";
|
|
}
|
|
if ($service->networks()->count() > 0) {
|
|
$commands[] = "echo 'Creating Docker network.'";
|
|
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
|
}
|
|
$commands[] = 'echo Starting service.';
|
|
$commands[] = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --remove-orphans --force-recreate --build < /dev/null";
|
|
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
|
|
if (data_get($service, 'connect_to_docker_network')) {
|
|
$compose = data_get($service, 'docker_compose', []);
|
|
$safeNetwork = escapeshellarg($service->destination->network);
|
|
$serviceNames = data_get(parseDockerComposeYaml($compose), 'services', []);
|
|
foreach ($serviceNames as $serviceName => $serviceConfig) {
|
|
$containerName = escapeshellarg("{$serviceName}-{$service->uuid}");
|
|
$commands[] = "docker network connect --alias {$containerName} {$safeNetwork} {$containerName} >/dev/null 2>&1 || true";
|
|
}
|
|
}
|
|
$commands = array_merge($commands, $this->logDrainNetworkConnectCommands($service));
|
|
|
|
return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStartFinished', callEventData: $service->id, queue: deployment_queue());
|
|
}
|
|
|
|
/**
|
|
* Shows the volume warnings of the last parse (for example an external volume that the
|
|
* service does not use yet) in the start log.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public static function composeVolumeWarningCommands(Service $service): array
|
|
{
|
|
return array_map(
|
|
fn (string $warning): string => 'echo '.escapeshellarg("Warning: {$warning}"),
|
|
$service->composeVolumeWarnings()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, LocalFileVolume>
|
|
*/
|
|
private function contentFileStorages(Service $service): Collection
|
|
{
|
|
return LocalFileVolume::query()
|
|
->where(function ($query) use ($service) {
|
|
$query->where('resource_type', (new ServiceApplication)->getMorphClass())
|
|
->whereIn('resource_id', $service->applications()->select('id'));
|
|
})
|
|
->orWhere(function ($query) use ($service) {
|
|
$query->where('resource_type', (new ServiceDatabase)->getMorphClass())
|
|
->whereIn('resource_id', $service->databases()->select('id'));
|
|
})
|
|
->get();
|
|
}
|
|
|
|
private function logDrainNetworkConnectCommands(Service $service): array
|
|
{
|
|
if (! data_get($service, 'connect_to_docker_network')) {
|
|
return [];
|
|
}
|
|
|
|
if (! $service->destination?->server?->isLogDrainEnabled()) {
|
|
return [];
|
|
}
|
|
|
|
$network = data_get($service, 'destination.network');
|
|
|
|
if (blank($network)) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
'docker network connect '.escapeshellarg($network).' coolify-log-drain >/dev/null 2>&1 || true',
|
|
];
|
|
}
|
|
|
|
private function shouldStopBeforeStarting(bool $pullLatestImages, bool $stopBeforeStart): bool
|
|
{
|
|
return $stopBeforeStart && ! $pullLatestImages;
|
|
}
|
|
}
|