diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php
index 2e9afe52d..f5ecf1fe1 100644
--- a/app/Http/Livewire/Project/Application/General.php
+++ b/app/Http/Livewire/Project/Application/General.php
@@ -4,6 +4,7 @@
use App\Models\Application;
use App\Models\InstanceSettings;
+use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
@@ -14,7 +15,7 @@ class General extends Component
public string $applicationId;
public Application $application;
- public ?array $services = null;
+ public Collection $services;
public string $name;
public string|null $fqdn;
public string $git_repository;
@@ -33,6 +34,7 @@ class General extends Component
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
+ public array $service_configurations = [];
protected $rules = [
'application.name' => 'required',
@@ -54,6 +56,8 @@ class General extends Component
'application.dockercompose_raw' => 'nullable',
'application.dockercompose' => 'nullable',
'application.service_configurations.*' => 'nullable',
+ 'service_configurations.*.fqdn' => 'nullable|url',
+ 'service_configurations.*.port' => 'integer',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -74,6 +78,8 @@ class General extends Component
'application.dockerfile' => 'Dockerfile',
'application.dockercompose_raw' => 'Docker Compose (raw)',
'application.dockercompose' => 'Docker Compose',
+ 'service_configurations.*.fqdn' => 'FQDN',
+ 'service_configurations.*.port' => 'Port',
];
@@ -95,8 +101,8 @@ public function instantSave()
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
- $this->emit('success', 'Application settings updated!');
$this->checkWildCardDomain();
+ $this->emit('success', 'Application settings updated!');
}
protected function checkWildCardDomain()
@@ -109,6 +115,7 @@ protected function checkWildCardDomain()
public function mount()
{
+ $this->services = $this->application->services();
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
@@ -117,8 +124,8 @@ public function mount()
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->checkWildCardDomain();
- if (data_get($this->application, 'dockercompose_raw')) {
- $this->services = data_get(Yaml::parse($this->application->dockercompose_raw), 'services');
+ if (data_get($this->application, 'service_configurations')) {
+ $this->service_configurations = $this->application->service_configurations;
}
}
@@ -149,8 +156,8 @@ public function generateServerRandomDomain()
public function submit()
{
try {
- ray($this->application->service_configurations);
- // $this->validate();
+ $this->application->service_configurations = $this->service_configurations;
+ $this->validate();
if (data_get($this->application, 'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
@@ -170,7 +177,7 @@ public function submit()
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
if (data_get($this->application, 'dockercompose_raw')) {
- $details = generateServiceFromTemplate($this->application->dockercompose_raw, $this->application);
+ $details = generateServiceFromTemplate( $this->application);
$this->application->dockercompose = data_get($details, 'dockercompose');
}
$this->application->save();
diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php
index b699aea35..fabc43aca 100644
--- a/app/Http/Livewire/Project/Application/Heading.php
+++ b/app/Http/Livewire/Project/Application/Heading.php
@@ -64,7 +64,7 @@ public function stop()
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
- remote_process(
+ instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
diff --git a/app/Http/Livewire/Project/New/DockerCompose.php b/app/Http/Livewire/Project/New/DockerCompose.php
index 6701443e7..d74cdb368 100644
--- a/app/Http/Livewire/Project/New/DockerCompose.php
+++ b/app/Http/Livewire/Project/New/DockerCompose.php
@@ -7,11 +7,15 @@
use App\Models\GithubApp;
use App\Models\LocalPersistentVolume;
use App\Models\Project;
+use App\Models\Service;
+use App\Models\ServiceApplication;
+use App\Models\ServiceDatabase;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
+use Symfony\Component\Yaml\Yaml;
class DockerCompose extends Component
{
@@ -70,68 +74,81 @@ public function submit()
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
- $application = Application::create([
- 'name' => 'dockercompose-' . new Cuid2(7),
- 'repository_project_id' => 0,
- 'fqdn' => 'https://app.coolify.io',
- 'git_repository' => "coollabsio/coolify",
- 'git_branch' => 'main',
- 'build_pack' => 'dockercompose',
- 'ports_exposes' => '0',
- 'dockercompose_raw' => $this->dockercompose,
- 'environment_id' => $environment->id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination_class,
- 'source_id' => 0,
- 'source_type' => GithubApp::class
- ]);
- $fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io";
- if (isDev()) {
- $fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
- }
- $application->update([
- 'name' => 'dockercompose-' . $application->uuid,
- 'fqdn' => $fqdn,
- ]);
+ $service = new Service();
+ $service->uuid = (string) new Cuid2(7);
+ $service->name = 'service-' . new Cuid2(7);
+ $service->docker_compose_raw = $this->dockercompose;
+ $service->environment_id = $environment->id;
+ $service->destination_id = $destination->id;
+ $service->destination_type = $destination_class;
+ $service->save();
+ $service->parse(saveIt: true);
- $details = generateServiceFromTemplate($this->dockercompose, $application);
- $envs = data_get($details, 'envs', []);
- if ($envs->count() > 0) {
- foreach ($envs as $env) {
- $key = Str::of($env)->before('=');
- $value = Str::of($env)->after('=');
- EnvironmentVariable::create([
- 'key' => $key,
- 'value' => $value,
- 'is_build_time' => false,
- 'application_id' => $application->id,
- 'is_preview' => false,
- ]);
- }
- }
- $volumes = data_get($details, 'volumes', []);
- if ($volumes->count() > 0) {
- foreach ($volumes as $volume => $mount_path) {
- LocalPersistentVolume::create([
- 'name' => $volume,
- 'mount_path' => $mount_path,
- 'resource_id' => $application->id,
- 'resource_type' => $application->getMorphClass(),
- 'is_readonly' => false
- ]);
- }
- }
- $dockercompose_coolified = data_get($details, 'dockercompose', '');
- $application->update([
- 'dockercompose' => $dockercompose_coolified,
- 'ports_exposes' => data_get($details, 'ports', 0)->implode(','),
- ]);
-
-
- redirect()->route('project.application.configuration', [
- 'application_uuid' => $application->uuid,
+ return redirect()->route('project.service', [
+ 'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
+ // $compose = data_get($parsedService, 'docker_compose');
+ // $service->docker_compose = $compose;
+ // $shouldDefine = data_get($parsedService, 'should_define', collect([]));
+ // if ($shouldDefine->count() > 0) {
+ // $envs = data_get($shouldDefine, 'envs', []);
+ // foreach($envs as $env) {
+ // ray($env);
+ // $variableName = Str::of($env)->before('=');
+ // $variableValue = Str::of($env)->after('=');
+ // ray($variableName, $variableValue);
+ // }
+ // }
+ // foreach ($services as $serviceName => $serviceDetails) {
+ // if (data_get($serviceDetails,'is_database')) {
+ // $serviceDatabase = new ServiceDatabase();
+ // $serviceDatabase->name = $serviceName . '-' . $service->uuid;
+ // $serviceDatabase->service_id = $service->id;
+ // $serviceDatabase->save();
+ // } else {
+ // $serviceApplication = new ServiceApplication();
+ // $serviceApplication->name = $serviceName . '-' . $service->uuid;
+ // $serviceApplication->fqdn =
+ // $serviceApplication->service_id = $service->id;
+ // $serviceApplication->save();
+ // }
+ // }
+
+ // ray($details);
+ // $envs = data_get($details, 'envs', []);
+ // if ($envs->count() > 0) {
+ // foreach ($envs as $env) {
+ // $key = Str::of($env)->before('=');
+ // $value = Str::of($env)->after('=');
+ // EnvironmentVariable::create([
+ // 'key' => $key,
+ // 'value' => $value,
+ // 'is_build_time' => false,
+ // 'service_id' => $service->id,
+ // 'is_preview' => false,
+ // ]);
+ // }
+ // }
+ // $volumes = data_get($details, 'volumes', []);
+ // if ($volumes->count() > 0) {
+ // foreach ($volumes as $volume => $mount_path) {
+ // LocalPersistentVolume::create([
+ // 'name' => $volume,
+ // 'mount_path' => $mount_path,
+ // 'resource_id' => $service->id,
+ // 'resource_type' => $service->getMorphClass(),
+ // 'is_readonly' => false
+ // ]);
+ // }
+ // }
+ // $dockercompose_coolified = data_get($details, 'dockercompose', '');
+ // $service->update([
+ // 'docker_compose' => $dockercompose_coolified,
+ // ]);
+
+
+
}
}
diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php
new file mode 100644
index 000000000..17429d315
--- /dev/null
+++ b/app/Http/Livewire/Project/Service/Index.php
@@ -0,0 +1,24 @@
+parameters = get_route_parameters();
+ $this->query = request()->query();
+ $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
+ }
+ public function render()
+ {
+ return view('livewire.project.service.index')->layout('layouts.app');
+ }
+}
diff --git a/app/Http/Livewire/Service/Index.php b/app/Http/Livewire/Service/Index.php
new file mode 100644
index 000000000..fbf651084
--- /dev/null
+++ b/app/Http/Livewire/Service/Index.php
@@ -0,0 +1,25 @@
+parameters = get_route_parameters();
+ $this->query = request()->query();
+ $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
+ ray($this->service->docker_compose);
+ }
+ public function render()
+ {
+ return view('livewire.project.service.index')->layout('layouts.app');
+ }
+}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 837a74a5f..816742d8f 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -73,6 +73,7 @@ public function __construct(int $application_deployment_queue_id)
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id);
+ $isService = $this->application->services()->count() > 0;
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
@@ -128,7 +129,7 @@ public function handle(): void
try {
if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
- } else if($this->application->dockercompose) {
+ } else if ($this->application->services()->count() > 0) {
$this->deploy_docker_compose();
} else {
if ($this->pull_request_id !== 0) {
@@ -168,7 +169,8 @@ public function handle(): void
);
}
}
- private function deploy_docker_compose() {
+ private function deploy_docker_compose()
+ {
$dockercompose_base64 = base64_encode($this->application->dockercompose);
$this->execute_remote_command(
[
@@ -184,9 +186,26 @@ private function deploy_docker_compose() {
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->save_environment_variables();
- $this->start_by_compose_file();
+ $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
+ 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 save_environment_variables() {
+ private function save_environment_variables()
+ {
$envs = collect([]);
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
@@ -197,7 +216,6 @@ private function save_environment_variables() {
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
-
}
private function deploy_simple_dockerfile()
{
diff --git a/app/Models/Application.php b/app/Models/Application.php
index bc2172ae9..2286ace22 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -4,12 +4,17 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity;
+use Symfony\Component\Yaml\Yaml;
+use Illuminate\Support\Str;
class Application extends BaseModel
{
protected $guarded = [];
-
+ protected $casts = [
+ 'service_configurations' => 'array',
+ ];
protected static function booted()
{
static::created(function ($application) {
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 301ea6e85..18b4e5438 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -33,12 +33,14 @@ protected static function booted()
}
});
}
-
+ public function service() {
+ return $this->belongsTo(Service::class);
+ }
protected function value(): Attribute
{
return Attribute::make(
- get: fn (string $value) => $this->get_environment_variables($value),
- set: fn (string $value) => $this->set_environment_variables($value),
+ get: fn (?string $value = null) => $this->get_environment_variables($value),
+ set: fn (?string $value = null) => $this->set_environment_variables($value),
);
}
@@ -57,8 +59,11 @@ private function get_environment_variables(string $environment_variable): string
return $environment_variable;
}
- private function set_environment_variables(string $environment_variable): string|null
+ private function set_environment_variables(?string $environment_variable = null): string|null
{
+ if (is_null($environment_variable) && $environment_variable == '') {
+ return null;
+ }
$environment_variable = trim($environment_variable);
return encrypt($environment_variable);
}
@@ -69,4 +74,5 @@ protected function key(): Attribute
set: fn (string $value) => Str::of($value)->trim(),
);
}
+
}
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index a35f31cdc..20ea51c63 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -14,6 +14,10 @@ public function application()
{
return $this->morphTo();
}
+ public function service()
+ {
+ return $this->morphTo();
+ }
public function standalone_postgresql()
{
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 1fd19e119..13df241d6 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -76,6 +76,9 @@ public function settings()
return $this->hasOne(ServerSetting::class);
}
+ public function proxyType() {
+ return $this->proxy->get('type');
+ }
public function scopeWithProxy(): Builder
{
return $this->proxy->modelScope();
diff --git a/app/Models/Service.php b/app/Models/Service.php
new file mode 100644
index 000000000..c4d492948
--- /dev/null
+++ b/app/Models/Service.php
@@ -0,0 +1,267 @@
+morphTo();
+ }
+ public function persistentStorages()
+ {
+ return $this->morphMany(LocalPersistentVolume::class, 'resource');
+ }
+ public function portsMappingsArray(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => is_null($this->ports_mappings)
+ ? []
+ : explode(',', $this->ports_mappings),
+
+ );
+ }
+ public function portsExposesArray(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => is_null($this->ports_exposes)
+ ? []
+ : explode(',', $this->ports_exposes)
+ );
+ }
+ public function applications()
+ {
+ return $this->hasMany(ServiceApplication::class);
+ }
+ public function databases()
+ {
+ return $this->hasMany(ServiceDatabase::class);
+ }
+ public function environment_variables(): HasMany
+ {
+ return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
+ }
+ public function parse(bool $saveIt = false): Collection
+ {
+ if ($this->docker_compose_raw) {
+ ray()->clearAll();
+ $yaml = Yaml::parse($this->docker_compose_raw);
+
+ $composeVolumes = collect(data_get($yaml, 'volumes', []));
+ $composeNetworks = collect(data_get($yaml, 'networks', []));
+ $services = data_get($yaml, 'services');
+ $definedNetwork = data_get($this, 'destination.network');
+
+ $volumes = collect([]);
+ $envs = collect([]);
+ $ports = collect([]);
+
+ $services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $saveIt) {
+ $isDatabase = false;
+ // Decide if the service is a database
+ $image = data_get($service, 'image');
+ if ($image) {
+ $imageName = Str::of($image)->before(':');
+ if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
+ $isDatabase = true;
+ data_set($service, 'is_database', true);
+ }
+ }
+ if ($saveIt) {
+ if ($isDatabase) {
+ $savedService = ServiceDatabase::create([
+ 'name' => $serviceName,
+ 'service_id' => $this->id
+ ]);
+ } else {
+ $savedService = ServiceApplication::create([
+ 'name' => $serviceName,
+ 'service_id' => $this->id
+ ]);
+ }
+ }
+ // Collect ports
+ $servicePorts = collect(data_get($service, 'ports', []));
+ $ports->put($serviceName, $servicePorts);
+ if ($saveIt) {
+ $savedService->ports_exposes = $servicePorts->implode(',');
+ $savedService->save();
+ }
+ // Collect volumes
+ $serviceVolumes = collect(data_get($service, 'volumes', []));
+ if ($serviceVolumes->count() > 0) {
+ foreach ($serviceVolumes as $volume) {
+ if (is_string($volume)) {
+ $volumeName = Str::before($volume, ':');
+ $volumePath = Str::after($volume, ':');
+ }
+ if (is_array($volume)) {
+ $volumeName = data_get($volume, 'source');
+ $volumePath = data_get($volume, 'target');
+ }
+
+ $volumeExists = $serviceVolumes->contains(function ($_, $key) use ($volumeName) {
+ return $key == $volumeName;
+ });
+ if (!$volumeExists) {
+ if (!Str::startsWith($volumeName, '/')) {
+ $composeVolumes->put($volumeName, null);
+ }
+ $volumes->put($volumeName, $volumePath);
+ if ($saveIt) {
+ LocalPersistentVolume::create([
+ 'name' => $volumeName,
+ 'mount_path' => $volumePath,
+ 'resource_id' => $savedService->id,
+ 'resource_type' => get_class($savedService)
+ ]);
+ }
+ }
+ }
+ }
+
+ // Collect and add networks
+ $serviceNetworks = collect(data_get($service, 'networks', []));
+ if ($serviceNetworks->count() > 0) {
+ foreach ($serviceNetworks as $networkName => $networkDetails) {
+ $networkExists = $composeNetworks->contains(function ($value, $key) use ($networkName) {
+ return $value == $networkName || $key == $networkName;
+ });
+ if (!$networkExists) {
+ $composeNetworks->put($networkName, null);
+ }
+ }
+ }
+ // Add Coolify specific networks
+ $definedNetworkExists = $composeNetworks->contains(function ($value, $_) use ($definedNetwork) {
+ return $value == $definedNetwork;
+ });
+ if (!$definedNetworkExists) {
+ $composeNetworks->put($definedNetwork, [
+ 'external' => true
+ ]);
+ }
+
+ // Get variables from the service that does not start with SERVICE_*
+ $serviceVariables = collect(data_get($service, 'environment', []));
+ foreach ($serviceVariables as $variable) {
+ $value = Str::after($variable, '=');
+ if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) {
+ $value = Str::of(replaceVariables(Str::of($value)));
+ if ($value->contains(':')) {
+ $nakedName = $value->before(':');
+ $nakedValue = $value->after(':');
+ } else if ($value->contains('-')) {
+ $nakedName = $value->before('-');
+ $nakedValue = $value->after('-');
+ } else if ($value->contains('+')) {
+ $nakedName = $value->before('+');
+ $nakedValue = $value->after('+');
+ } else {
+ $nakedName = $value;
+ }
+ if (isset($nakedName)) {
+ if (isset($nakedValue)) {
+ if ($nakedValue->startsWith('-')) {
+ $nakedValue = Str::of($nakedValue)->after('-');
+ }
+ if ($nakedValue->startsWith('+')) {
+ $nakedValue = Str::of($nakedValue)->after('+');
+ }
+ if (!$envs->has($nakedName->value())) {
+ $envs->put($nakedName->value(), $nakedValue->value());
+ if ($saveIt) {
+ EnvironmentVariable::create([
+ 'key' => $nakedName->value(),
+ 'value' => $nakedValue->value(),
+ 'is_build_time' => false,
+ 'service_id' => $this->id,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ } else {
+ if (!$envs->has($nakedName->value())) {
+ $envs->put($nakedName->value(), null);
+ if ($saveIt) {
+ EnvironmentVariable::create([
+ 'key' => $nakedName->value(),
+ 'value' => null,
+ 'is_build_time' => false,
+ 'service_id' => $this->id,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ }
+ }
+ } else {
+ $value = Str::of(replaceVariables(Str::of($value)));
+ $generatedValue = null;
+ if ($value->startsWith('SERVICE_USER')) {
+ $generatedValue = Str::random(10);
+ if ($saveIt) {
+ if (!$envs->has($value->value())) {
+ $envs->put($value->value(), $generatedValue);
+ EnvironmentVariable::create([
+ 'key' => $value->value(),
+ 'value' => $generatedValue,
+ 'is_build_time' => false,
+ 'service_id' => $this->id,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ } else if ($value->startsWith('SERVICE_PASSWORD')) {
+ $generatedValue = Str::password(symbols: false);
+ if ($saveIt) {
+ if (!$envs->has($value->value())) {
+ $envs->put($value->value(), $generatedValue);
+ EnvironmentVariable::create([
+ 'key' => $value->value(),
+ 'value' => $generatedValue,
+ 'is_build_time' => false,
+ 'service_id' => $this->id,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ }
+ }
+ }
+
+ data_forget($service, 'is_database');
+ data_forget($service, 'documentation');
+ return $service;
+ });
+ data_set($services, 'volumes', $composeVolumes->toArray());
+ data_set($services, 'networks', $composeNetworks->toArray());
+ $this->docker_compose = Yaml::parse($services);
+ // $compose = Str::of(Yaml::dump($services, 10, 2));
+ // TODO: Replace SERVICE_FQDN_* with the actual FQDN
+ // TODO: Replace SERVICE_URL_*
+
+ $shouldBeDefined = collect([
+ 'envs' => $envs,
+ 'volumes' => $volumes,
+ 'ports' => $ports
+ ]);
+ $parsedCompose = collect([
+ 'dockerCompose' => $services,
+ 'shouldBeDefined' => $shouldBeDefined
+ ]);
+ return $parsedCompose;
+ } else {
+ return collect([]);
+ }
+ }
+}
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
new file mode 100644
index 000000000..df506468c
--- /dev/null
+++ b/app/Models/ServiceApplication.php
@@ -0,0 +1,12 @@
+belongsTo(Server::class);
}
+ public function service()
+ {
+ return $this->belongsTo(Service::class, 'destination');
+ }
+
public function attachedTo()
{
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php
index a34a08339..b0fda78bf 100644
--- a/app/View/Components/Forms/Textarea.php
+++ b/app/View/Components/Forms/Textarea.php
@@ -25,7 +25,7 @@ public function __construct(
public bool $readonly = false,
public string|null $helper = null,
public bool $realtimeValidation = false,
- public string $defaultClass = "textarea bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
+ public string $defaultClass = "textarea leading-normal bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) {
//
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index cea780c24..cf89caf5a 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -10,3 +10,16 @@
'yearly' => '0 0 1 1 *',
];
const RESTART_MODE = 'unless-stopped';
+
+const DATABASE_DOCKER_IMAGES = [
+ 'mysql',
+ 'mariadb',
+ 'postgres',
+ 'mongo',
+ 'redis',
+ 'memcached',
+ 'couchdb',
+ 'neo4j',
+ 'influxdb',
+ 'clickhouse'
+];
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 191668e0a..7925bbacc 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -130,7 +130,64 @@ function get_port_from_dockerfile($dockerfile): int
return 80;
}
-function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null)
+function defaultLabels($id, $name, $pull_request_id = 0)
+{
+ $labels = collect([]);
+ $labels->push('coolify.managed=true');
+ $labels->push('coolify.version=' . config('version'));
+ $labels->push('coolify.applicationId=' . $id);
+ $labels->push('coolify.type=application');
+ $labels->push('coolify.name=' . $name);
+ if ($pull_request_id !== 0) {
+ $labels->push('coolify.pullRequestId=' . $pull_request_id);
+ }
+ return $labels;
+}
+function fqdnLabelsForTraefik($domain, $container_name, $is_force_https_enabled)
+{
+ $labels = collect([]);
+ $labels->push('traefik.enable=true');
+ $url = Url::fromString($domain);
+ $host = $url->getHost();
+ $path = $url->getPath();
+ $schema = $url->getScheme();
+ $slug = Str::slug($host . $path);
+
+ $http_label = "{$container_name}-{$slug}-http";
+ $https_label = "{$container_name}-{$slug}-https";
+
+ if ($schema === 'https') {
+ // Set labels for https
+ $labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
+ $labels->push("traefik.http.routers.{$https_label}.entryPoints=https");
+ $labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
+ if ($path !== '/') {
+ $labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix");
+ $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
+ }
+
+ $labels->push("traefik.http.routers.{$https_label}.tls=true");
+ $labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
+
+ // Set labels for http (redirect to https)
+ $labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
+ $labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
+ if ($is_force_https_enabled) {
+ $labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
+ }
+ } else {
+ // Set labels for http
+ $labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
+ $labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
+ $labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
+ if ($path !== '/') {
+ $labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix");
+ $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
+ }
+ }
+ return $labels;
+}
+function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
{
$pull_request_id = data_get($preview, 'pull_request_id', 0);
@@ -139,15 +196,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
if ($pull_request_id !== 0) {
$appId = $appId . '-pr-' . $application->pull_request_id;
}
- $labels = [];
- $labels[] = 'coolify.managed=true';
- $labels[] = 'coolify.version=' . config('version');
- $labels[] = 'coolify.applicationId=' . $appId;
- $labels[] = 'coolify.type=application';
- $labels[] = 'coolify.name=' . $application->name;
- if ($pull_request_id !== 0) {
- $labels[] = 'coolify.pullRequestId=' . $pull_request_id;
- }
+ $labels = collect([]);
+ $labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id));
if ($application->fqdn) {
if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@@ -155,48 +205,48 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
if ($application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) {
- $labels[] = 'traefik.enable=true';
foreach ($domains as $domain) {
- $url = Url::fromString($domain);
- $host = $url->getHost();
- $path = $url->getPath();
- $schema = $url->getScheme();
- $slug = Str::slug($host . $path);
+ $labels = $labels->merge(fqdnLabelsForTraefik($domain, $container_name, $application->settings->is_force_https_enabled));
+ // $url = Url::fromString($domain);
+ // $host = $url->getHost();
+ // $path = $url->getPath();
+ // $schema = $url->getScheme();
+ // $slug = Str::slug($host . $path);
- $http_label = "{$container_name}-{$slug}-http";
- $https_label = "{$container_name}-{$slug}-https";
+ // $http_label = "{$container_name}-{$slug}-http";
+ // $https_label = "{$container_name}-{$slug}-https";
- if ($schema === 'https') {
- // Set labels for https
- $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https";
- $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip";
- if ($path !== '/') {
- $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix";
- $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}";
- }
+ // if ($schema === 'https') {
+ // // Set labels for https
+ // $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ // $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https";
+ // $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip";
+ // if ($path !== '/') {
+ // $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix";
+ // $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}";
+ // }
- $labels[] = "traefik.http.routers.{$https_label}.tls=true";
- $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt";
+ // $labels[] = "traefik.http.routers.{$https_label}.tls=true";
+ // $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt";
- // Set labels for http (redirect to https)
- $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
- if ($application->settings->is_force_https_enabled) {
- $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https";
- }
- } else {
- // Set labels for http
- $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
- $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip";
- if ($path !== '/') {
- $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix";
- $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}";
- }
- }
+ // // Set labels for http (redirect to https)
+ // $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ // $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ // if ($application->settings->is_force_https_enabled) {
+ // $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https";
+ // }
+ // } else {
+ // // Set labels for http
+ // $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ // $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ // $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip";
+ // if ($path !== '/') {
+ // $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix";
+ // $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}";
+ // }
+ // }
}
}
}
- return $labels;
+ return $labels->all();
}
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index bd6fc6e11..eebeae72c 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -1,6 +1,8 @@
clearAll();
-
- $template = Str::of($template);
- $network = data_get($application, 'destination.network');
+ $template = data_get($service, 'docker_compose_raw');
+ $network = data_get($service, 'destination.network');
$yaml = Yaml::parse($template);
- $services = data_get($yaml, 'services');
+
+ $services = $service->parse();
$volumes = collect(data_get($yaml, 'volumes', []));
$composeVolumes = collect([]);
$env = collect([]);
$ports = collect([]);
foreach ($services as $serviceName => $service) {
+ $container_name = generateApplicationContainerName($application);
+ $domain = data_get($application, "service_configurations.{$serviceName}.fqdn", null);
+ if ($domain === '') {
+ $domain = null;
+ }
+ data_forget($service, 'documentation');
// Some default things
data_set($service, 'restart', RESTART_MODE);
- data_set($service, 'container_name', generateApplicationContainerName($application));
- $healthcheck = data_get($service, 'healthcheck', []);
+ data_set($service, 'container_name', $container_name);
+ $healthcheck = data_get($service, 'healthcheck');
if (is_null($healthcheck)) {
$healthcheck = [
'test' => [
@@ -41,6 +50,22 @@ function generateServiceFromTemplate(string $template, Application $application)
];
data_set($service, 'healthcheck', $healthcheck);
}
+ // Labels
+ $server = data_get($application, 'destination.server');
+ if ($server->proxyType() === ProxyTypes::TRAEFIK_V2->value) {
+ $labels = collect(data_get($service, 'labels', []));
+ $labels = collect([]);
+ $labels = $labels->merge(defaultLabels($application->id, $container_name));
+ if (!data_get($service, 'is_database')) {
+ if ($domain) {
+ $labels = $labels->merge(fqdnLabelsForTraefik($domain, $container_name, $application->settings->is_force_https_enabled));
+ }
+
+ }
+ data_set($service, 'labels', $labels->toArray());
+ }
+
+ data_forget($service, 'is_database');
// Add volumes to the volumes collection if they don't already exist
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -76,7 +101,7 @@ function generateServiceFromTemplate(string $template, Application $application)
// Get variables from the service that does not start with SERVICE_*
$serviceVariables = collect(data_get($service, 'environment', []));
foreach ($serviceVariables as $variable) {
- $key = Str::before($variable, '=');
+ // $key = Str::before($variable, '=');
$value = Str::after($variable, '=');
if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) {
if (Str::of($value)->contains(':')) {
@@ -111,7 +136,7 @@ function generateServiceFromTemplate(string $template, Application $application)
}
data_set($yaml, 'networks', [
$network => [
- 'name'=> $network
+ 'name' => $network
],
]);
data_set($yaml, 'volumes', $composeVolumes->toArray());
diff --git a/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php b/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php
deleted file mode 100644
index e2953776c..000000000
--- a/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php
+++ /dev/null
@@ -1,30 +0,0 @@
-longText('dockercompose_raw')->nullable();
- $table->longText('dockercompose')->nullable();
- $table->json('service_configurations')->nullable();
-
- });
- }
-
- public function down(): void
- {
- Schema::table('applications', function (Blueprint $table) {
- $table->dropColumn('dockercompose_raw');
- $table->dropColumn('dockercompose');
- $table->dropColumn('service_configurations');
- });
- }
-};
diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php
new file mode 100644
index 000000000..0abdafac3
--- /dev/null
+++ b/database/migrations/2023_09_20_082541_update_services_table.php
@@ -0,0 +1,31 @@
+longText('docker_compose_raw');
+ $table->longText('docker_compose')->nullable();
+
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('services', function (Blueprint $table) {
+ $table->dropColumn('docker_compose_raw');
+ $table->dropColumn('docker_compose');
+ });
+ }
+};
diff --git a/database/migrations/2023_09_20_082733_create_service_databases_table.php b/database/migrations/2023_09_20_082733_create_service_databases_table.php
new file mode 100644
index 000000000..841888349
--- /dev/null
+++ b/database/migrations/2023_09_20_082733_create_service_databases_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->string('uuid')->unique();
+ $table->string('name');
+
+ $table->string('ports_exposes')->nullable();
+ $table->string('ports_mappings')->nullable();
+
+ $table->foreignId('service_id');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('service_databases');
+ }
+};
diff --git a/database/migrations/2023_09_20_082737_create_service_applications_table.php b/database/migrations/2023_09_20_082737_create_service_applications_table.php
new file mode 100644
index 000000000..1c1f0edb7
--- /dev/null
+++ b/database/migrations/2023_09_20_082737_create_service_applications_table.php
@@ -0,0 +1,50 @@
+id();
+ $table->string('uuid')->unique();
+ $table->string('name');
+
+ $table->string('fqdn')->unique()->nullable();
+
+ $table->string('ports_exposes')->nullable();
+ $table->string('ports_mappings')->nullable();
+
+ $table->string('health_check_path')->default('/');
+ $table->string('health_check_port')->nullable();
+ $table->string('health_check_host')->default('localhost');
+ $table->string('health_check_method')->default('GET');
+ $table->integer('health_check_return_code')->default(200);
+ $table->string('health_check_scheme')->default('http');
+ $table->string('health_check_response_text')->nullable();
+ $table->integer('health_check_interval')->default(5);
+ $table->integer('health_check_timeout')->default(5);
+ $table->integer('health_check_retries')->default(10);
+ $table->integer('health_check_start_period')->default(5);
+
+ $table->string('status')->default('exited');
+
+ $table->foreignId('service_id');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('service_applications');
+ }
+};
diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
new file mode 100644
index 000000000..40eb6aa44
--- /dev/null
+++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
@@ -0,0 +1,29 @@
+foreignId('service_id')->nullable();
+
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('service_id');
+ });
+ }
+};
diff --git a/database/seeders/ServiceApplicationSeeder.php b/database/seeders/ServiceApplicationSeeder.php
new file mode 100644
index 000000000..94d523cf4
--- /dev/null
+++ b/database/seeders/ServiceApplicationSeeder.php
@@ -0,0 +1,17 @@
+