Compare commits

...

77 Commits

Author SHA1 Message Date
Andras Bacsai
cfdab13d77 Merge pull request #1250 from coollabsio/next
v4.0.0-beta.45
2023-09-24 21:41:58 +02:00
Andras Bacsai
4fc8988ff4 fix: put back build pack chooser 2023-09-24 21:38:11 +02:00
Andras Bacsai
ab5619292e Merge pull request #1249 from chz/fix/1240
ui: Fixed z-index for magicbar
2023-09-24 21:18:48 +02:00
Andras Bacsai
b02e5d3f27 fix: applications with port mappins do a normal update (not rolling update) 2023-09-24 21:15:43 +02:00
Andras Bacsai
5f0c9c3a31 ui: Add source button 2023-09-24 21:15:21 +02:00
Chingiz Mammadov
ea6ec07a45 ui: Fixed z-index for magicbar 2023-09-24 23:03:11 +04:00
Andras Bacsai
36be325d0d Merge pull request #1248 from chz/ui/fix-version-zindex
ui: Fixed z-index for version link.
2023-09-24 20:42:35 +02:00
Andras Bacsai
6138ddcac6 revert sending notification 2023-09-24 17:51:01 +02:00
Andras Bacsai
0509da6730 fix: also check docker socks permission on validation 2023-09-24 17:48:40 +02:00
Andras Bacsai
5b877b84c2 fix: add expose port for containers 2023-09-24 17:48:25 +02:00
Andras Bacsai
0c35726a8d fix: add traefik labels no matter if traefik is selected or not 2023-09-24 17:47:43 +02:00
Andras Bacsai
e74899611b fix: links with path 2023-09-24 17:39:12 +02:00
Andras Bacsai
e9149e534d temporary disable containerstatusjob failed 2023-09-24 17:31:41 +02:00
Chingiz Mammadov
7cded7a36d ui: Fixed z-index for version link. 2023-09-24 14:12:07 +04:00
Andras Bacsai
ba74d55b4c fix: report livewire errors 2023-09-24 12:10:36 +02:00
Andras Bacsai
a1d13fc14e Merge pull request #1247 from coollabsio/next
v4.0.0-beta.44 - fix schema
2023-09-24 11:58:36 +02:00
Andras Bacsai
cdb2a3a8e5 version-- 2023-09-24 11:58:10 +02:00
Andras Bacsai
074c56edaa version++ 2023-09-24 11:57:11 +02:00
Andras Bacsai
66bc03b4cd fix: database schema 2023-09-24 11:56:32 +02:00
Andras Bacsai
2dcb7fec05 fix: services 2023-09-24 11:52:13 +02:00
Andras Bacsai
249bccb49e Merge pull request #1246 from coollabsio/next
v4.0.0-beta.44
2023-09-24 11:39:36 +02:00
Andras Bacsai
a55eaa10ac ui: fix typo 2023-09-24 11:32:26 +02:00
Andras Bacsai
ca2c75ce19 fix: only show traefik dashboard if its available 2023-09-24 11:10:50 +02:00
Andras Bacsai
250d7cbc53 ui: fixes 2023-09-24 10:55:15 +02:00
Andras Bacsai
92a53a151e fix: server validation with cf tunnels 2023-09-24 10:48:54 +02:00
Andras Bacsai
db3148c080 typos 2023-09-24 10:32:41 +02:00
Andras Bacsai
c46eeac4b5 feat: add cloudflare tunnel support 2023-09-23 13:34:40 +02:00
Andras Bacsai
19111ba059 fix: startProxy 2023-09-23 12:21:56 +02:00
Andras Bacsai
9bc61a0a17 fix: make sure proxy path created 2023-09-23 11:53:30 +02:00
Andras Bacsai
78b9166bdb fix: sentry issue 4478125289 2023-09-23 11:47:24 +02:00
Andras Bacsai
1752448050 wip: services 2023-09-22 21:31:47 +02:00
Andras Bacsai
3fc78bc760 wip: services 2023-09-22 20:39:56 +02:00
Andras Bacsai
0c30b6222d fix: mappings 2023-09-22 15:49:59 +02:00
Andras Bacsai
3f74609c7f remove debug 2023-09-22 15:47:21 +02:00
Andras Bacsai
31b0ccba99 fix: prevent overwrite already existing env variables in services 2023-09-22 15:46:30 +02:00
Andras Bacsai
3fc544e0b9 feat: healthcheck for apps 2023-09-22 15:29:19 +02:00
Andras Bacsai
67078fdc71 wip: services
feat: able to map port<->domain
2023-09-22 14:47:25 +02:00
Andras Bacsai
c91f426af3 wip: services 2023-09-22 12:08:51 +02:00
Andras Bacsai
9c2fea4b2e fix: delete persistent storages on resource deletion 2023-09-22 11:34:27 +02:00
Andras Bacsai
53d1fa0331 wip: ui for services 2023-09-22 11:23:49 +02:00
Andras Bacsai
4ae7e46e81 fix: proxy connections 2023-09-22 08:52:07 +02:00
Andras Bacsai
ebfc0bd1e1 fix: add proxy to network with periodic check 2023-09-22 08:42:27 +02:00
Andras Bacsai
e1a1490911 wip 2023-09-21 21:30:13 +02:00
Andras Bacsai
6b75ff7de4 wip: services 2023-09-21 17:48:31 +02:00
Andras Bacsai
301469de6b fix: save proxy configuration 2023-09-21 11:03:52 +02:00
Andras Bacsai
b4d69a22df wip: services 2023-09-20 15:42:41 +02:00
Andras Bacsai
a86e971020 wip: services 2023-09-19 15:51:13 +02:00
Andras Bacsai
145af41c82 fix: delete environment variables on app/db delete 2023-09-19 14:08:20 +02:00
Andras Bacsai
69c0b7240a fix: add github app change on new app view 2023-09-19 13:30:17 +02:00
Andras Bacsai
6543132bcb test 7 days trial 2023-09-19 09:03:27 +02:00
Andras Bacsai
9134437218 version++ 2023-09-18 15:43:14 +02:00
Andras Bacsai
b7acf99cde Merge pull request #1238 from coollabsio/next
Fixes
2023-09-18 15:31:54 +02:00
Andras Bacsai
21d52d7846 oops 2023-09-18 15:23:23 +02:00
Andras Bacsai
ab5929cc69 typo 2023-09-18 15:20:48 +02:00
Andras Bacsai
1452cdf5ad fix: send internal notifications of email errors 2023-09-18 15:19:27 +02:00
Andras Bacsai
3eb1a1f48c debug with ray 2023-09-18 15:06:37 +02:00
Andras Bacsai
5f7a97c31f debug job 2023-09-18 15:04:50 +02:00
Andras Bacsai
9cba0a6df3 fix: boarding again 2023-09-18 14:41:31 +02:00
Andras Bacsai
af57b2aa73 pricing change 2023-09-18 13:42:35 +02:00
Andras Bacsai
b9b9582601 version++ 2023-09-18 13:14:48 +02:00
Andras Bacsai
c8ba98b93d fix: try to use old docker-compose 2023-09-18 13:14:05 +02:00
Andras Bacsai
a50e1e2f0c Merge pull request #1237 from coollabsio/next
Lots of fixes
2023-09-18 13:06:43 +02:00
Andras Bacsai
1093294f06 minimum toaster 2023-09-18 13:06:02 +02:00
Andras Bacsai
c023be2348 fix: improve localhost boarding process 2023-09-18 13:01:01 +02:00
Andras Bacsai
deece51e83 fix: stop/start UI on apps and dbs 2023-09-18 12:38:11 +02:00
Andras Bacsai
e2ab569244 version++ 2023-09-18 12:30:49 +02:00
Andras Bacsai
93b202bde4 fix: convert startProxy to action 2023-09-18 12:29:50 +02:00
Andras Bacsai
6bb6de188c Merge pull request #1236 from coollabsio/next
v4.0.0-beta.41
2023-09-18 12:26:00 +02:00
Andras Bacsai
61f58fa30f fix: proxy configuration saving 2023-09-18 12:18:45 +02:00
Andras Bacsai
026f3fd72d fix 2023-09-18 12:09:31 +02:00
Andras Bacsai
9a706d55b4 fix: proxy container status 2023-09-18 12:08:19 +02:00
Andras Bacsai
9646969107 fix: errors 2023-09-18 11:49:26 +02:00
Andras Bacsai
df433efe62 fix: boarding 2023-09-18 11:21:10 +02:00
Andras Bacsai
efa7cab3e2 fix: installDocker id not found 2023-09-18 10:44:32 +02:00
Andras Bacsai
a386a1bde5 fix: allow non ip address (DNS) 2023-09-18 10:41:19 +02:00
Andras Bacsai
cc1bf023be fix: do not remove localhost in boarding 2023-09-18 10:41:06 +02:00
Andras Bacsai
edb06fa233 version++ 2023-09-18 10:06:35 +02:00
146 changed files with 3215 additions and 945 deletions

View File

@@ -21,6 +21,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
]; ];
@@ -96,6 +97,7 @@ class StartPostgresql
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server); return remote_process($this->commands, $server);
} }

View File

@@ -2,21 +2,27 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CheckConfigurationSync class CheckConfiguration
{ {
public function __invoke(Server $server, bool $reset = false) use AsAction;
public function handle(Server $server, bool $reset = false)
{ {
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$proxy_configuration = instant_remote_process([ $proxy_configuration = instant_remote_process([
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml", "cat $proxy_path/docker-compose.yml",
], $server, false); ], $server, false);
if ($reset || is_null($proxy_configuration)) { if ($reset || !$proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
} }
if (!$proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception("Could not generate proxy configuration");
}
return $proxy_configuration; return $proxy_configuration;
} }
} }

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveConfiguration
{
use AsAction;
public function handle(Server $server, ?string $proxy_settings = null)
{
if (is_null($proxy_settings)) {
$proxy_settings = CheckConfiguration::run($server, true);
}
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
class SaveConfigurationSync
{
public function __invoke(Server $server)
{
try {
$proxy_settings = resolve(CheckConfigurationSync::class)($server, true);
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
} catch (\Throwable $e) {
ray($e);
}
}
}

View File

@@ -2,54 +2,41 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
class StartProxy class StartProxy
{ {
public function __invoke(Server $server, bool $async = true): Activity|string use AsAction;
public function handle(Server $server, bool $async = true): Activity|string
{ {
$proxyType = data_get($server,'proxy.type'); $commands = collect([]);
$proxyType = $server->proxyType();
if ($proxyType === 'none') { if ($proxyType === 'none') {
return 'OK'; return 'OK';
} }
if (is_null($proxyType)) {
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$server->proxy->status = ProxyStatus::EXITED->value;
$server->save();
}
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) { $configuration = CheckConfiguration::run($server);
return $docker['network']; if (!$configuration) {
})->unique(); throw new \Exception("Configuration is not synced");
if ($networks->count() === 0) {
$networks = collect(['coolify']);
} }
$create_networks_command = $networks->map(function ($network) {
return "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1";
});
$configuration = resolve(CheckConfigurationSync::class)($server);
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $server->save();
$commands = [
"command -v lsof >/dev/null || echo '####### Installing lsof...'", $commands = $commands->merge([
"command -v lsof >/dev/null || apt-get update", "apt-get update > /dev/null 2>&1 || true",
"command -v lsof >/dev/null || echo '####### Installing lsof.'",
"command -v lsof >/dev/null || apt install -y lsof", "command -v lsof >/dev/null || apt install -y lsof",
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc", "command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc",
"echo '####### Creating required Docker networks...'", "mkdir -p $proxy_path && cd $proxy_path",
...$create_networks_command, "echo '####### Creating Docker Compose file.'",
"cd $proxy_path", "echo '####### Pulling docker image.'",
"echo '####### Creating Docker Compose file...'",
"echo '####### Pulling docker image...'",
'docker compose pull', 'docker compose pull',
"echo '####### Stopping existing coolify-proxy...'", "echo '####### Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1 || true", "docker compose down -v --remove-orphans > /dev/null 2>&1",
"command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'", "command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'",
"command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true", "command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true",
"command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true", "command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true",
@@ -58,10 +45,11 @@ class StartProxy
"systemctl disable nginx > /dev/null 2>&1 || true", "systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true", "systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true", "systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy...'", "echo '####### Starting coolify-proxy.'",
'docker compose up -d --remove-orphans', 'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully...'" "echo '####### Proxy installed successfully.'"
]; ]);
$commands = $commands->merge(connectProxyToNetworks($server));
if (!$async) { if (!$async) {
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
return 'OK'; return 'OK';

View File

@@ -4,11 +4,10 @@ namespace App\Actions\Server;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\Team;
class InstallDocker class InstallDocker
{ {
public function __invoke(Server $server, Team $team) public function __invoke(Server $server)
{ {
$dockerVersion = '24.0'; $dockerVersion = '24.0';
$config = base64_encode('{ $config = base64_encode('{
@@ -19,15 +18,16 @@ class InstallDocker
} }
}'); }');
$found = StandaloneDocker::where('server_id', $server->id); $found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) { if ($found->count() == 0 && $server->id) {
StandaloneDocker::create([ StandaloneDocker::create([
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} }
if (isDev()) {
return remote_process([ if (isDev() && $server->id === 0) {
$command = [
"echo '####### Installing Prerequisites...'", "echo '####### Installing Prerequisites...'",
"sleep 1", "sleep 1",
"echo '####### Installing/updating Docker Engine...'", "echo '####### Installing/updating Docker Engine...'",
@@ -35,9 +35,9 @@ class InstallDocker
"sleep 4", "sleep 4",
"echo '####### Restarting Docker Engine...'", "echo '####### Restarting Docker Engine...'",
"ls -l /tmp" "ls -l /tmp"
], $server); ];
} else { } else {
return remote_process([ $command = [
"echo '####### Installing Prerequisites...'", "echo '####### Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update", "command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq", "command -v jq >/dev/null || apt install -y jq",
@@ -53,7 +53,8 @@ class InstallDocker
"echo '####### Creating default Docker network (coolify)...'", "echo '####### Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true", "docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'" "echo '####### Done!'"
], $server); ];
} }
return remote_process($command, $server);
} }
} }

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StartService
{
use AsAction;
public function handle(Service $service)
{
$workdir = service_configuration_dir() . "/{$service->uuid}";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "mkdir -p $workdir";
$commands[] = "cd $workdir";
$docker_compose_base64 = base64_encode($service->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml";
$envs = $service->environment_variables()->get();
$commands[] = "rm -f .env || true";
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
}
$commands[] = "docker compose pull --quiet";
$commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d >/dev/null 2>&1";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$activity = remote_process($commands, $service->server);
return $activity;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StopService
{
use AsAction;
public function handle(Service $service)
{
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
}
}

View File

@@ -17,8 +17,11 @@ class CoolifyTaskArgs extends Data
public string $type, public string $type,
public ?string $type_uuid = null, public ?string $type_uuid = null,
public ?Model $model = null, public ?Model $model = null,
public string $status = ProcessStatus::QUEUED->value, public ?string $status = null ,
public bool $ignore_errors = false, public bool $ignore_errors = false,
) { ) {
if(is_null($status)){
$this->status = ProcessStatus::QUEUED->value;
}
} }
} }

View File

@@ -9,12 +9,12 @@ use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Index extends Component class Index extends Component
{ {
public string $currentState = 'welcome'; public string $currentState = 'welcome';
public ?string $selectedServerType = null;
public ?Collection $privateKeys = null; public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null; public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
@@ -37,6 +37,11 @@ class Index extends Component
public ?int $selectedExistingProject = null; public ?int $selectedExistingProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
public bool $dockerInstallationStarted = false;
public string $serverPublicKey;
public bool $serverReachable = true;
public function mount() public function mount()
{ {
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
@@ -64,12 +69,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function restartBoarding() public function restartBoarding()
{ {
if ($this->createdServer) {
$this->createdServer->delete();
}
if ($this->createdPrivateKey) {
$this->createdPrivateKey->delete();
}
return redirect()->route('boarding'); return redirect()->route('boarding');
} }
public function skipBoarding() public function skipBoarding()
@@ -84,13 +83,15 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function setServerType(string $type) public function setServerType(string $type)
{ {
if ($type === 'localhost') { $this->selectedServerType = $type;
if ($this->selectedServerType === 'localhost') {
$this->createdServer = Server::find(0); $this->createdServer = Server::find(0);
if (!$this->createdServer) { if (!$this->createdServer) {
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
} }
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($type === 'remote') { } elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) { if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id; $this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
@@ -113,9 +114,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return; return;
} }
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->validateServer(); $this->validateServer();
$this->getProxyType();
$this->getProjects();
} }
public function getProxyType() public function getProxyType()
{ {
@@ -128,6 +128,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
} }
public function selectExistingPrivateKey() public function selectExistingPrivateKey()
{ {
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->currentState = 'create-server'; $this->currentState = 'create-server';
} }
public function createNewServer() public function createNewServer()
@@ -150,87 +151,81 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'privateKeyName' => 'required', 'privateKeyName' => 'required',
'privateKey' => 'required', 'privateKey' => 'required',
]); ]);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id
]);
$this->createdPrivateKey->save();
$this->currentState = 'create-server'; $this->currentState = 'create-server';
} }
public function saveServer() public function saveServer()
{ {
$this->validate([ $this->validate([
'remoteServerName' => 'required', 'remoteServerName' => 'required',
'remoteServerHost' => 'required|ip', 'remoteServerHost' => 'required',
'remoteServerPort' => 'required|integer', 'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required', 'remoteServerUser' => 'required',
]); ]);
$this->privateKey = formatPrivateKey($this->privateKey); $this->privateKey = formatPrivateKey($this->privateKey);
$this->createdPrivateKey = new PrivateKey();
$this->createdPrivateKey->private_key = $this->privateKey;
$this->createdPrivateKey->name = $this->privateKeyName;
$this->createdPrivateKey->description = $this->privateKeyDescription;
$this->createdPrivateKey->team_id = currentTeam()->id;
$foundServer = Server::whereIp($this->remoteServerHost)->first(); $foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) { if ($foundServer) {
return $this->emit('error', 'IP address is already in use by another team.'); return $this->emit('error', 'IP address is already in use by another team.');
} }
$this->createdServer = new Server(); $this->createdServer = Server::create([
$this->createdServer->uuid = (string)new Cuid2(7);
$this->createdServer->name = $this->remoteServerName;
$this->createdServer->ip = $this->remoteServerHost;
$this->createdServer->port = $this->remoteServerPort;
$this->createdServer->user = $this->remoteServerUser;
$this->createdServer->description = $this->remoteServerDescription;
$this->createdServer->privateKey = $this->createdPrivateKey;
$this->createdServer->team_id = currentTeam()->id;
ray($this->createdServer);
$this->validateServer();
}
public function validateServer(?string $type = null)
{
try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true);
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
throw new \Exception('No Docker Engine or older than 23 version installed.');
}
$customErrorMessage = "Cannot create Server or Private Key. Please try again.";
if ($type !== 'localhost') {
$createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id
]);
$server = Server::create([
'name' => $this->remoteServerName, 'name' => $this->remoteServerName,
'ip' => $this->remoteServerHost, 'ip' => $this->remoteServerHost,
'port' => $this->remoteServerPort, 'port' => $this->remoteServerPort,
'user' => $this->remoteServerUser, 'user' => $this->remoteServerUser,
'description' => $this->remoteServerDescription, 'description' => $this->remoteServerDescription,
'private_key_id' => $createdPrivateKey->id, 'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
$server->settings->is_reachable = true; $this->createdServer->save();
$server->settings->is_usable = true; $this->validateServer();
$server->settings->save(); }
} else { public function validateServer()
$this->createdServer->settings->update([ {
try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true);
$this->createdServer->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
]); ]);
} catch (\Throwable $e) {
$this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
} }
try {
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.');
}
$this->createdServer->settings()->update([
'is_usable' => true,
]);
$this->getProxyType(); $this->getProxyType();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
} }
} }
public function installDocker() public function installDocker()
{ {
$activity = resolve(InstallDocker::class)($this->createdServer, currentTeam()); $this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->createdServer);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
$this->currentState = 'select-proxy'; }
public function dockerInstalledOrSkipped()
{
$this->validateServer();
} }
public function selectProxy(string|null $proxyType = null) public function selectProxy(string|null $proxyType = null)
{ {

View File

@@ -38,7 +38,7 @@ class Form extends Component
$this->destination->delete(); $this->destination->delete();
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
} }

View File

@@ -78,7 +78,7 @@ class StandaloneDocker extends Component
private function createNetworkAndAttachToProxy() private function createNetworkAndAttachToProxy()
{ {
instant_remote_process(['docker network create --attachable ' . $this->network], $this->server, throwError: false); $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process(["docker network connect $this->network coolify-proxy"], $this->server, throwError: false); instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
} }

View File

@@ -4,15 +4,18 @@ namespace App\Http\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component class General extends Component
{ {
public string $applicationId; public string $applicationId;
public Application $application; public Application $application;
public Collection $services;
public string $name; public string $name;
public string|null $fqdn; public string|null $fqdn;
public string $git_repository; public string $git_repository;
@@ -31,6 +34,7 @@ class General extends Component
public bool $is_auto_deploy_enabled; public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled; public bool $is_force_https_enabled;
protected $rules = [ protected $rules = [
'application.name' => 'required', 'application.name' => 'required',
'application.description' => 'nullable', 'application.description' => 'nullable',
@@ -66,6 +70,7 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes', 'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings', 'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile', 'application.dockerfile' => 'Dockerfile',
]; ];
public function instantSave() public function instantSave()
@@ -86,8 +91,8 @@ class General extends Component
$this->application->settings->save(); $this->application->settings->save();
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkWildCardDomain(); $this->checkWildCardDomain();
$this->emit('success', 'Application settings updated!');
} }
protected function checkWildCardDomain() protected function checkWildCardDomain()
@@ -136,7 +141,6 @@ class General extends Component
public function submit() public function submit()
{ {
ray($this->application);
try { try {
$this->validate(); $this->validate();
if (data_get($this->application, 'fqdn')) { if (data_get($this->application, 'fqdn')) {
@@ -145,7 +149,7 @@ class General extends Component
}); });
$this->application->fqdn = $domains->implode(','); $this->application->fqdn = $domains->implode(',');
} }
if ($this->application->dockerfile) { if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile); $port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) { if ($port) {
$this->application->ports_exposes = $port; $this->application->ports_exposes = $port;

View File

@@ -21,7 +21,7 @@ class Heading extends Component
public function check_status() public function check_status()
{ {
dispatch_sync(new ContainerStatusJob($this->application->destination->server)); dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh(); $this->application->refresh();
$this->application->previews->each(function ($preview) { $this->application->previews->each(function ($preview) {
$preview->refresh(); $preview->refresh();
@@ -64,14 +64,15 @@ class Heading extends Component
foreach ($containers as $container) { foreach ($containers as $container) {
$containerName = data_get($container, 'Names'); $containerName = data_get($container, 'Names');
if ($containerName) { if ($containerName) {
remote_process( instant_remote_process(
["docker rm -f {$containerName}"], ["docker rm -f {$containerName}"],
$this->application->destination->server $this->application->destination->server
); );
$this->application->status = 'stopped'; $this->application->status = 'exited';
$this->application->save(); $this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application)); // $this->application->environment->project->team->notify(new StatusChanged($this->application));
} }
} }
$this->application->refresh();
} }
} }

View File

@@ -72,7 +72,7 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id); $container_name = generateApplicationContainerName($this->application);
ray('Stopping container: ' . $container_name); ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false); instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);

View File

@@ -43,7 +43,7 @@ class Heading extends Component
stopPostgresProxy($this->database); stopPostgresProxy($this->database);
$this->database->is_public = false; $this->database->is_public = false;
} }
$this->database->status = 'stopped'; $this->database->status = 'exited';
$this->database->save(); $this->database->save();
$this->check_status(); $this->check_status();
// $this->database->environment->project->team->notify(new StatusChanged($this->database)); // $this->database->environment->project->team->notify(new StatusChanged($this->database));

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Project;
use App\Models\Service;
use Livewire\Component;
use Illuminate\Support\Str;
class DockerCompose extends Component
{
public string $dockercompose = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
$this->dockercompose = 'services:
ghost:
documentation: https://ghost.org/docs/config
image: ghost:5
volumes:
- ghost-content-data:/var/lib/ghost/content
environment:
- url=$SERVICE_FQDN_GHOST
- database__client=mysql
- database__connection__host=mysql
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
- database__connection__database=${MYSQL_DATABASE-ghost}
depends_on:
- mysql
mysql:
documentation: https://hub.docker.com/_/mysql
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
';
}
}
public function submit()
{
$this->validate([
'dockercompose' => 'required'
]);
$server_id = $this->query['server_id'];
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$service = Service::create([
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockercompose,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}

View File

@@ -9,8 +9,8 @@ use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect; use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {
@@ -40,21 +40,6 @@ class GithubPrivateRepository extends Component
public string|null $publish_directory = null; public string|null $publish_directory = null;
protected int $page = 1; protected int $page = 1;
// public function saveFromRedirect(string $route, ?Collection $parameters = null){
// session()->forget('from');
// if (!$parameters || $parameters->count() === 0) {
// $parameters = $this->parameters;
// }
// $parameters = collect($parameters) ?? collect([]);
// $queries = collect($this->query) ?? collect([]);
// $parameters = $parameters->merge($queries);
// session(['from'=> [
// 'back'=> $this->currentRoute,
// 'route' => $route,
// 'parameters' => $parameters
// ]]);
// return redirect()->route($route);
// }
public function mount() public function mount()
{ {
@@ -159,6 +144,13 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io";
if (isDev()) {
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save();
redirect()->route('project.application.configuration', [ redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,

View File

@@ -112,6 +112,13 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io";
if (isDev()) {
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_random_name($application->uuid);
$application->save();
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,

View File

@@ -137,7 +137,6 @@ class PublicGitRepository extends Component
$project = Project::where('uuid', $project_uuid)->first(); $project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $environment_name)->first(); $environment = $project->load(['environments'])->environments->where('name', $environment_name)->first();
$application_init = [ $application_init = [
'name' => generate_application_name($this->git_repository, $this->git_branch), 'name' => generate_application_name($this->git_repository, $this->git_branch),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
@@ -153,9 +152,17 @@ class PublicGitRepository extends Component
]; ];
$application = Application::create($application_init); $application = Application::create($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io";
if (isDev()) {
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save();
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,

View File

@@ -17,7 +17,7 @@ class Select extends Component
public string $type; public string $type;
public string $server_id; public string $server_id;
public string $destination_uuid; public string $destination_uuid;
public Countable|array|Server $servers; public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = []; public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = []; public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
@@ -83,6 +83,7 @@ class Select extends Component
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
'type' => $this->type, 'type' => $this->type,
'destination' => $this->destination_uuid, 'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
]); ]);
} }

View File

@@ -59,8 +59,14 @@ CMD ["nginx", "-g", "daemon off;"]
'source_id' => 0, 'source_id' => 0,
'source_type' => GithubApp::class '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([ $application->update([
'name' => 'dockerfile-' . $application->id 'name' => 'dockerfile-' . $application->uuid,
'fqdn' => $fqdn
]); ]);
redirect()->route('project.application.configuration', [ redirect()->route('project.application.configuration', [

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Application extends Component
{
public ServiceApplication $application;
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
];
public function render()
{
ray($this->application->fileStorages()->get());
return view('livewire.project.service.application');
}
public function submit()
{
try {
$this->validate();
$this->application->save();
} catch (\Throwable $e) {
ray($e);
} finally {
$this->emit('generateDockerCompose');
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Livewire\Component;
class Database extends Component
{
public ServiceDatabase $database;
protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
];
public function render()
{
return view('livewire.project.service.database');
}
public function submit()
{
try {
$this->validate();
$this->database->save();
} catch (\Throwable $e) {
ray($e);
} finally {
$this->emit('generateDockerCompose');
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalFileVolume;
use Livewire\Component;
class FileStorage extends Component
{
public LocalFileVolume $fileStorage;
protected $rules = [
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
];
public function render()
{
return view('livewire.project.service.file-storage');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use Livewire\Component;
class Index extends Component
{
public Service $service;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'required',
];
public function mount()
{
$this->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');
}
public function save() {
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->emit('refreshEnvs');
}
public function submit() {
try {
$this->validate();
$this->service->save();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class Modal extends Component
{
public function serviceStatusUpdated() {
$this->emit('serviceStatusUpdated');
}
public function render()
{
return view('livewire.project.service.modal');
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
class Navbar extends Component
{
public Service $service;
public array $parameters;
public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render()
{
return view('livewire.project.service.navbar');
}
public function serviceStatusUpdated()
{
ray('serviceStatusUpdated');
$this->check_status();
}
public function check_status()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->service->refresh();
}
public function deploy()
{
$this->service->parse();
$activity = StartService::run($this->service);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()
{
StopService::run($this->service);
$this->service->refresh();
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
{
public Service $service;
public ServiceApplication $serviceApplication;
public ServiceDatabase $serviceDatabase;
public array $parameters;
public array $query;
public Collection $services;
protected $listeners = ['generateDockerCompose'];
public function mount()
{
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
}
}
public function generateDockerCompose()
{
$this->service->parse();
}
public function render()
{
return view('livewire.project.service.show');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Shared; namespace App\Http\Livewire\Project\Shared;
use App\Actions\Service\StopService;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -19,13 +20,25 @@ class Danger extends Component
public function delete() public function delete()
{ {
try {
if ($this->resource->type() === 'service') {
$server = $this->resource->server;
StopService::run($this->resource);
} else {
$destination = data_get($this->resource, 'destination');
if ($destination) {
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first(); $destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server;
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $destination->server); }
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
}
$this->resource->delete(); $this->resource->delete();
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'] 'environment_name' => $this->parameters['environment_name']
]); ]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View File

@@ -31,7 +31,6 @@ class Add extends Component
public function submit() public function submit()
{ {
ray('submitting');
$this->validate(); $this->validate();
$this->emitUp('submit', [ $this->emitUp('submit', [
'key' => $this->key, 'key' => $this->key,

View File

@@ -53,6 +53,7 @@ class All extends Component
$this->resource->environment_variables_preview()->delete(); $this->resource->environment_variables_preview()->delete();
} else { } else {
$variables = parseEnvFormatToArray($this->variables); $variables = parseEnvFormatToArray($this->variables);
ray($variables);
$existingVariables = $this->resource->environment_variables(); $existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete(); $this->resource->environment_variables()->delete();
} }
@@ -68,11 +69,16 @@ class All extends Component
$environment->value = $variable; $environment->value = $variable;
$environment->is_build_time = false; $environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false; $environment->is_preview = $isPreview ? true : false;
if ($this->resource->type() === 'application') { switch ($this->resource->type()) {
case 'application':
$environment->application_id = $this->resource->id; $environment->application_id = $this->resource->id;
} break;
if ($this->resource->type() === 'standalone-postgresql') { case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id; $environment->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
} }
$environment->save(); $environment->save();
} }

View File

@@ -10,7 +10,9 @@ class Show extends Component
{ {
public $parameters; public $parameters;
public ModelsEnvironmentVariable $env; public ModelsEnvironmentVariable $env;
public string|null $modalId = null; public ?string $modalId = null;
public string $type;
protected $rules = [ protected $rules = [
'env.key' => 'required|string', 'env.key' => 'required|string',
'env.value' => 'required|string', 'env.value' => 'required|string',
@@ -37,6 +39,7 @@ class Show extends Component
$this->validate(); $this->validate();
$this->env->save(); $this->env->save();
$this->emit('success', 'Environment variable updated successfully.'); $this->emit('success', 'Environment variable updated successfully.');
$this->emit('refreshEnvs');
} }
public function delete() public function delete()

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class HealthChecks extends Component
{
public $resource;
protected $rules = [
'resource.health_check_path' => 'string',
'resource.health_check_port' => 'nullable|string',
'resource.health_check_host' => 'string',
'resource.health_check_method' => 'string',
'resource.health_check_return_code' => 'integer',
'resource.health_check_scheme' => 'string',
'resource.health_check_response_text' => 'nullable|string',
'resource.health_check_interval' => 'integer',
'resource.health_check_timeout' => 'integer',
'resource.health_check_retries' => 'integer',
'resource.health_check_start_period' => 'integer',
];
public function submit()
{
try {
$this->validate();
$this->resource->save();
$this->emit('saved');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.health-checks');
}
}

View File

@@ -2,13 +2,16 @@
namespace App\Http\Livewire\Project\Shared\Storages; namespace App\Http\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Show extends Component class Show extends Component
{ {
public $storage; public LocalPersistentVolume $storage;
public string|null $modalId = null; public bool $isReadOnly = false;
public ?string $modalId = null;
protected $rules = [ protected $rules = [
'storage.name' => 'required|string', 'storage.name' => 'required|string',
'storage.mount_path' => 'required|string', 'storage.mount_path' => 'required|string',

View File

@@ -15,6 +15,7 @@ class Form extends Component
public $dockerVersion; public $dockerVersion;
public string|null $wildcard_domain = null; public string|null $wildcard_domain = null;
public int $cleanup_after_percentage; public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false;
protected $rules = [ protected $rules = [
'server.name' => 'required|min:6', 'server.name' => 'required|min:6',
@@ -22,6 +23,7 @@ class Form extends Component
'server.ip' => 'required', 'server.ip' => 'required',
'server.user' => 'required', 'server.user' => 'required',
'server.port' => 'required', 'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required',
'server.settings.is_reachable' => 'required', 'server.settings.is_reachable' => 'required',
'server.settings.is_part_of_swarm' => 'required', 'server.settings.is_part_of_swarm' => 'required',
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
@@ -32,6 +34,7 @@ class Form extends Component
'server.ip' => 'ip', 'server.ip' => 'ip',
'server.user' => 'user', 'server.user' => 'user',
'server.port' => 'port', 'server.port' => 'port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable', 'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm' 'server.settings.is_part_of_swarm' => 'is part of swarm'
]; ];
@@ -41,10 +44,15 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function instantSave() {
refresh_server_connection($this->server->privateKey);
$this->validateServer();
$this->server->settings->save();
}
public function installDocker() public function installDocker()
{ {
$activity = resolve(InstallDocker::class)($this->server, currentTeam()); $this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->server);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
@@ -61,13 +69,14 @@ class Form extends Component
} }
if ($dockerVersion) { if ($dockerVersion) {
$this->dockerVersion = $dockerVersion; $this->dockerVersion = $dockerVersion;
$this->emit('proxyStatusUpdated');
$this->emit('success', 'Docker Engine 23+ is installed!'); $this->emit('success', 'Docker Engine 23+ is installed!');
} else { } else {
$this->emit('error', 'No Docker Engine or older than 23 version installed.'); $this->emit('error', 'No Docker Engine or older than 23 version installed.');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this, customErrorMessage: "Server is not reachable: "); return handleError($e, $this, customErrorMessage: "Server is not reachable: ");
} finally {
$this->emit('proxyStatusUpdated');
} }
} }

View File

@@ -26,7 +26,7 @@ class ByIp extends Component
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'description' => 'nullable|string', 'description' => 'nullable|string',
'ip' => 'required|ip', 'ip' => 'required',
'user' => 'required|string', 'user' => 'required|string',
'port' => 'required|integer', 'port' => 'required|integer',
]; ];
@@ -79,7 +79,7 @@ class ByIp extends Component
$server->settings->save(); $server->settings->save();
return redirect()->route('server.show', $server->uuid); return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
} }

View File

@@ -2,11 +2,11 @@
namespace App\Http\Livewire\Server; namespace App\Http\Livewire\Server;
use App\Actions\Proxy\CheckConfigurationSync; use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfigurationSync; use App\Actions\Proxy\SaveConfiguration;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str;
class Proxy extends Component class Proxy extends Component
{ {
@@ -48,34 +48,38 @@ class Proxy extends Component
public function submit() public function submit()
{ {
try { try {
resolve(SaveConfigurationSync::class)($this->server); SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url; $this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save(); $this->server->save();
setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server); setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server);
$this->emit('success', 'Proxy configuration saved.'); $this->emit('success', 'Proxy configuration saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
public function reset_proxy_configuration() public function reset_proxy_configuration()
{ {
try { try {
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server, true); $this->proxy_settings = CheckConfiguration::run($this->server, true);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
public function loadProxyConfiguration() public function loadProxyConfiguration()
{ {
try { try {
ray('loadProxyConfiguration'); $this->proxy_settings = CheckConfiguration::run($this->server);
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server); if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) {
$this->emit('traefikDashboardAvailable', true);
} else {
$this->emit('traefikDashboardAvailable', false);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
} }

View File

@@ -2,7 +2,6 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\SaveConfigurationSync;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -10,23 +9,28 @@ use Livewire\Component;
class Deploy extends Component class Deploy extends Component
{ {
public Server $server; public Server $server;
public $proxy_settings = null; public bool $traefikDashboardAvailable = false;
protected $listeners = ['proxyStatusUpdated']; public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable'];
public function proxyStatusUpdated() { public function mount() {
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data) {
$this->traefikDashboardAvailable = $data;
}
public function proxyStatusUpdated()
{
$this->server->refresh(); $this->server->refresh();
} }
public function startProxy() public function startProxy()
{ {
if ( try {
$this->server->proxy->last_applied_settings && $activity = StartProxy::run($this->server);
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
resolve(SaveConfigurationSync::class)($this->server);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function stop() public function stop()

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -18,13 +19,11 @@ class Status extends Component
{ {
try { try {
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$container = getContainerStatus(server: $this->server, container_id: 'coolify-proxy'); dispatch_sync(new ContainerStatusJob($this->server));
$this->server->proxy->status = $container;
$this->server->save();
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e, $this);
} }
} }
public function getProxyStatusWithNoti() public function getProxyStatusWithNoti()

View File

@@ -37,14 +37,27 @@ class ShowPrivateKey extends Component
try { try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true); ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true);
if ($uptime) { if ($uptime) {
$this->server->settings->update([
'is_reachable' => true
]);
$this->emit('success', 'Server is reachable with this private key.'); $this->emit('success', 'Server is reachable with this private key.');
} else { } else {
$this->server->settings->update([
'is_reachable' => false,
'is_usable' => false
]);
$this->emit('error', 'Server is not reachable with this private key.'); $this->emit('error', 'Server is not reachable with this private key.');
return; return;
} }
if ($dockerVersion) { if ($dockerVersion) {
$this->server->settings->update([
'is_usable' => true
]);
$this->emit('success', 'Server is usable for Coolify.'); $this->emit('success', 'Server is usable for Coolify.');
} else { } else {
$this->server->settings->update([
'is_usable' => false
]);
$this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.'); $this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -37,6 +37,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private int $application_deployment_queue_id; private int $application_deployment_queue_id;
private bool $newVersionIsHealthy = false;
private ApplicationDeploymentQueue $application_deployment_queue; private ApplicationDeploymentQueue $application_deployment_queue;
private Application $application; private Application $application;
private string $deployment_uuid; private string $deployment_uuid;
@@ -88,15 +89,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/'); $this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/');
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id); $this->container_name = generateApplicationContainerName($this->application);
addPrivateKeyToSshAgent($this->server); savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
// Set preview fqdn // Set preview fqdn
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) { if ($this->application->fqdn) {
$preview_fqdn = data_get($this->preview, 'fqdn'); $preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn); $url = Url::fromString($this->application->fqdn);
$host = $url->getHost(); $host = $url->getHost();
@@ -166,6 +167,54 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
} }
} }
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->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->save_environment_variables();
$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()
{
$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() private function deploy_simple_dockerfile()
{ {
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -235,10 +284,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if (count($this->application->ports_mappings_array) > 0){
$this->execute_remote_command(
["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file(); $this->start_by_compose_file();
$this->health_check(); $this->health_check();
$this->stop_running_container(); $this->stop_running_container();
} }
}
private function health_check() private function health_check()
{ {
ray('New container name: ', $this->container_name); ray('New container name: ', $this->container_name);
@@ -267,7 +327,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->execute_remote_command( $this->execute_remote_command(
[
"echo 'New version of your application is healthy.'"
],
[ [
"echo 'Rolling update completed.'" "echo 'Rolling update completed.'"
], ],
@@ -475,7 +539,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name, 'container_name' => $this->container_name,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => $this->set_labels_for_applications(), 'labels' => generateLabelsApplication($this->application, $this->preview),
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
@@ -577,75 +641,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $environment_variables->all(); return $environment_variables->all();
} }
private function set_labels_for_applications()
{
$appId = $this->application->id;
if ($this->pull_request_id !== 0) {
$appId = $appId . '-pr-' . $this->pull_request_id;
}
$labels = [];
$labels[] = 'coolify.managed=true';
$labels[] = 'coolify.version=' . config('version');
$labels[] = 'coolify.applicationId=' . $appId;
$labels[] = 'coolify.type=application';
$labels[] = 'coolify.name=' . $this->application->name;
if ($this->pull_request_id !== 0) {
$labels[] = 'coolify.pullRequestId=' . $this->pull_request_id;
}
if ($this->application->fqdn) {
if ($this->pull_request_id !== 0) {
$domains = Str::of(data_get($this->preview, 'fqdn'))->explode(',');
} else {
$domains = Str::of(data_get($this->application, 'fqdn'))->explode(',');
}
if ($this->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);
$http_label = "{$this->container_name}-{$slug}-http";
$https_label = "{$this->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}";
}
$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 ($this->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;
}
private function generate_healthcheck_commands() private function generate_healthcheck_commands()
{ {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') {
@@ -653,15 +648,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return 'exit 0'; return 'exit 0';
} }
if (!$this->application->health_check_port) { if (!$this->application->health_check_port) {
$this->application->health_check_port = $this->application->ports_exposes_array[0]; $health_check_port = $this->application->ports_exposes_array[0];
} else {
$health_check_port = $this->application->health_check_port;
} }
if ($this->application->health_check_path) { if ($this->application->health_check_path) {
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null" "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 { } else {
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/" "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); return implode(' ', $generated_healthchecks_commands);
@@ -718,20 +715,27 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
} }
private function stop_running_container() private function stop_running_container(bool $force = false)
{ {
if ($this->currently_running_container_name) { if ($this->currently_running_container_name) {
if ($this->newVersionIsHealthy || $force) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Removing old version of your application.'"], ["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
); );
} else {
$this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
);
}
} }
} }
private function start_by_compose_file() private function start_by_compose_file()
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Rolling update started.'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
); );
} }

View File

@@ -8,7 +8,6 @@ use App\Models\Server;
use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped; use App\Notifications\Container\ContainerStopped;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Arr;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUnique;
@@ -17,6 +16,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
@@ -40,7 +40,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->uuid; return $this->server->uuid;
} }
private function checkServerConnection() { private function checkServerConnection()
{
$uptime = instant_remote_process(['uptime'], $this->server, false); $uptime = instant_remote_process(['uptime'], $this->server, false);
if (!is_null($uptime)) { if (!is_null($uptime)) {
return true; return true;
@@ -51,7 +52,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
try { try {
// ray()->clearAll(); // ray()->clearAll();
$serverUptimeCheckNumber = 0; $serverUptimeCheckNumber = 0;
$serverUptimeCheckNumberMax = 5; $serverUptimeCheckNumberMax = 3;
while (true) { while (true) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
$this->server->settings()->update(['is_reachable' => false]); $this->server->settings()->update(['is_reachable' => false]);
@@ -65,25 +66,41 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$serverUptimeCheckNumber++; $serverUptimeCheckNumber++;
sleep(5); sleep(5);
} }
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server); $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications(); $applications = $this->server->applications();
$databases = $this->server->databases(); $databases = $this->server->databases();
$services = $this->server->services();
$previews = $this->server->previews(); $previews = $this->server->previews();
if ($this->server->isProxyShouldRun()) {
/// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) { $foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy'; return data_get($value, 'Name') === '/coolify-proxy';
})->first(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
resolve(StartProxy::class)($this->server, false); if ($this->server->isProxyShouldRun()) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); $this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} }
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
$foundApplications = []; $foundApplications = [];
$foundApplicationPreviews = []; $foundApplicationPreviews = [];
$foundDatabases = []; $foundDatabases = [];
$foundServices = [];
foreach ($containers as $container) { foreach ($containers as $container) {
$containerStatus = data_get($container, 'State.Status'); $containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status','unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels'); $labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels)); $labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId'); $labelId = data_get($labels, 'coolify.applicationId');
@@ -128,8 +145,60 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
} }
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$coolifyName = data_get($labels, 'coolify.name');
$serviceName = Str::of($coolifyName)->before('-');
$serviceUuid = Str::of($coolifyName)->after('-');
$service = $services->where('uuid', $serviceUuid)->first();
if ($service) {
$foundService = $service->byName($serviceName);
if ($foundService) {
$foundServices[] = "$foundService->id-$serviceName";
$statusFromDb = $foundService->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$foundService->update(['status' => $containerStatus]);
} }
}
}
}
}
$exitedServices = collect([]);
foreach ($services->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) {
continue;
} else {
$exitedServices->push($app);
}
}
foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) {
continue;
} else {
$exitedServices->push($db);
}
}
}
$exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) {
if ($exitedService->status === 'exited') {
continue;
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($service, 'environment.project');
$environment = data_get($service, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
$notRunningApplications = $applications->pluck('id')->diff($foundApplications); $notRunningApplications = $applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) { foreach ($notRunningApplications as $applicationId) {
$application = $applications->where('id', $applicationId)->first(); $application = $applications->where('id', $applicationId)->first();
@@ -188,95 +257,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid; $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }
return;
foreach ($applications as $application) {
$uuid = data_get($application, 'uuid');
$id = data_get($application, 'id');
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == $id) {
return $value;
}
$isPR = Str::startsWith(data_get($value, 'Name'), "/$uuid");
$isPR = Str::contains(data_get($value, 'Name'), "-pr-");
if ($isPR) {
return false;
}
return $value;
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($application, 'status');
if ($containerStatus !== $databaseStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($application, 'status');
if ($databaseStatus !== 'exited') {
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
$previews = $application->previews;
foreach ($previews as $preview) {
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid, $preview) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == "$id-pr-{$preview->id}") {
return $value;
}
return Str::startsWith(data_get($value, 'Name'), "/$uuid-pr-{$preview->id}");
})->first();
}
}
foreach ($databases as $database) {
$uuid = data_get($database, 'uuid');
$foundContainer = $containers->filter(function ($value, $key) use ($uuid) {
return Str::startsWith(data_get($value, 'Name'), "/$uuid");
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($database, 'status');
if ($containerStatus !== $databaseStatus) {
$database->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($database, 'status');
if ($databaseStatus !== 'exited') {
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
}
// TODO Monitor other containers not managed by Coolify
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());

View File

@@ -19,7 +19,13 @@ class Application extends BaseModel
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server);
}
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete();
$application->environment_variables_preview()->delete();
}); });
} }
@@ -224,7 +230,7 @@ class Application extends BaseModel
} }
public function git_based(): bool public function git_based(): bool
{ {
if ($this->dockerfile || $this->build_pack === 'dockerfile') { if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->dockercompose || $this->build_pack === 'dockercompose') {
return false; return false;
} }
return true; return true;

View File

@@ -33,18 +33,23 @@ class EnvironmentVariable extends Model
} }
}); });
} }
public function service() {
return $this->belongsTo(Service::class);
}
protected function value(): Attribute protected function value(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn (string $value) => $this->get_environment_variables($value), get: fn (?string $value = null) => $this->get_environment_variables($value),
set: fn (string $value) => $this->set_environment_variables($value), set: fn (?string $value = null) => $this->set_environment_variables($value),
); );
} }
private function get_environment_variables(string $environment_variable): string|null private function get_environment_variables(?string $environment_variable = null): string|null
{ {
// $team_id = currentTeam()->id; // $team_id = currentTeam()->id;
if (!$environment_variable) {
return null;
}
$environment_variable = trim(decrypt($environment_variable)); $environment_variable = trim(decrypt($environment_variable));
if (Str::startsWith($environment_variable, '{{') && Str::endsWith($environment_variable, '}}') && Str::contains($environment_variable, 'global.')) { if (Str::startsWith($environment_variable, '{{') && Str::endsWith($environment_variable, '}}') && Str::contains($environment_variable, 'global.')) {
$variable = Str::after($environment_variable, 'global.'); $variable = Str::after($environment_variable, 'global.');
@@ -57,8 +62,11 @@ class EnvironmentVariable extends Model
return $environment_variable; 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); $environment_variable = trim($environment_variable);
return encrypt($environment_variable); return encrypt($environment_variable);
} }
@@ -69,4 +77,5 @@ class EnvironmentVariable extends Model
set: fn (string $value) => Str::of($value)->trim(), set: fn (string $value) => Str::of($value)->trim(),
); );
} }
} }

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function service()
{
return $this->morphTo();
}
}

View File

@@ -14,6 +14,10 @@ class LocalPersistentVolume extends Model
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function service()
{
return $this->morphTo();
}
public function standalone_postgresql() public function standalone_postgresql()
{ {

View File

@@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
@@ -76,6 +78,15 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
} }
public function proxyType() {
$type = $this->proxy->get('type');
if (is_null($type)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value;
$this->save();
}
return $this->proxy->get('type');
}
public function scopeWithProxy(): Builder public function scopeWithProxy(): Builder
{ {
return $this->proxy->modelScope(); return $this->proxy->modelScope();
@@ -104,6 +115,9 @@ class Server extends BaseModel
return $standaloneDocker->applications; return $standaloneDocker->applications;
})->flatten(); })->flatten();
} }
public function services() {
return $this->hasMany(Service::class);
}
public function previews() { public function previews() {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {

447
app/Models/Service.php Normal file
View File

@@ -0,0 +1,447 @@
<?php
namespace App\Models;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
use Spatie\Url\Url;
class Service extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleted(function ($service) {
$storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) {
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$database->persistentStorages()->delete();
}
$service->environment_variables()->delete();
$service->applications()->delete();
$service->databases()->delete();
if ($storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($service) {
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
});
}
});
}
public function type()
{
return 'service';
}
public function applications()
{
return $this->hasMany(ServiceApplication::class);
}
public function databases()
{
return $this->hasMany(ServiceDatabase::class);
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function server()
{
return $this->belongsTo(Server::class);
}
public function byName(string $name)
{
$app = $this->applications()->whereName($name)->first();
if ($app) {
return $app;
}
$db = $this->databases()->whereName($name)->first();
if ($db) {
return $db;
}
return null;
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
}
public function parse(bool $isNew = false): Collection
{
ray('parsing');
// ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$composeVolumes = collect(data_get($yaml, 'volumes', []));
$composeNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$definedNetwork = $this->uuid;
$volumes = collect([]);
$envs = collect([]);
$ports = collect([]);
$services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew) {
$container_name = "$serviceName-{$this->uuid}";
$isDatabase = false;
$serviceVariables = collect(data_get($service, 'environment', []));
// 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 ($isNew) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'service_id' => $this->id
]);
} else {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io";
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
}
$savedService = ServiceApplication::create([
'name' => $serviceName,
'fqdn' => $defaultUsableFqdn,
'service_id' => $this->id
]);
}
} else {
if ($isDatabase) {
$savedService = $this->databases()->whereName($serviceName)->first();
} else {
$savedService = $this->applications()->whereName($serviceName)->first();
if (data_get($savedService, 'fqdn')) {
$defaultUsableFqdn = data_get($savedService, 'fqdn', null);
} else {
if (Str::of($serviceVariables)->contains('SERVICE_FQDN') || Str::of($serviceVariables)->contains('SERVICE_URL')) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io";
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
}
}
}
$savedService->fqdn = $defaultUsableFqdn;
$savedService->save();
}
}
$fqdns = data_get($savedService, 'fqdn');
if ($fqdns) {
$fqdns = collect(Str::of($fqdns)->explode(','));
}
// Collect ports
$servicePorts = collect(data_get($service, 'ports', []));
$ports->put($serviceName, $servicePorts);
$collectedPorts = collect([]);
if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) {
if (is_string($sport) || is_numeric($sport)) {
$collectedPorts->push($sport);
}
if (is_array($sport)) {
$target = data_get($sport, 'target');
$published = data_get($sport, 'published');
$collectedPorts->push("$target:$published");
}
}
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
// Collect volumes
$serviceVolumes = collect(data_get($service, 'volumes', []));
if ($serviceVolumes->count() > 0) {
LocalPersistentVolume::whereResourceId($savedService->id)->whereResourceType(get_class($savedService))->delete();
foreach ($serviceVolumes as $volume) {
if (is_string($volume) && Str::startsWith($volume, './')) {
// Local file
$fsPath = Str::before($volume, ':');
$volumePath = Str::of($volume)->after(':')->beforeLast(':');
ray($fsPath, $volumePath);
LocalFileVolume::updateOrCreate(
[
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $fsPath,
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
continue;
}
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, '/')) {
$volumes->put($volumeName, $volumePath);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => Str::slug($volumeName, '-'),
'mount_path' => $volumePath,
'host_path' => $volumeName,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else {
$composeVolumes->put($volumeName, null);
LocalPersistentVolume::updateOrCreate(
[
'name' => $volumeName,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => $volumeName,
'mount_path' => $volumePath,
'host_path' => null,
'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($networkDetails, null);
}
}
}
// Add Coolify specific networks
$definedNetworkExists = $composeNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
$composeNetworks->put($definedNetwork, [
'name' => $definedNetwork,
'external' => false
]);
}
$networks = $serviceNetworks->toArray();
$networks = array_merge($networks, [$definedNetwork]);
data_set($service, 'networks', $networks);
// Get variables from the service
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());
EnvironmentVariable::updateOrCreate([
'key' => $nakedName->value(),
'service_id' => $this->id,
], [
'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);
$envExists = EnvironmentVariable::where('service_id', $this->id)->where('key', $nakedName->value())->exists();
if (!$envExists) {
EnvironmentVariable::create([
'key' => $nakedName->value(),
'value' => null,
'service_id' => $this->id,
'is_build_time' => false,
'is_preview' => false,
]);
}
}
}
}
} else {
$variableName = Str::of(replaceVariables(Str::of($value)));
$generatedValue = null;
if ($variableName->startsWith('SERVICE_USER')) {
$variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first();
if (!$variableDefined) {
$generatedValue = Str::random(10);
} else {
$generatedValue = $variableDefined->value;
}
if (!$envs->has($variableName->value())) {
$envs->put($variableName->value(), $generatedValue);
EnvironmentVariable::updateOrCreate([
'key' => $variableName->value(),
'service_id' => $this->id,
], [
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
} else if ($variableName->startsWith('SERVICE_PASSWORD')) {
$variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first();
if (!$variableDefined) {
$generatedValue = Str::password(symbols: false);
} else {
$generatedValue = $variableDefined->value;
}
if (!$envs->has($variableName->value())) {
$envs->put($variableName->value(), $generatedValue);
EnvironmentVariable::updateOrCreate([
'key' => $variableName->value(),
'service_id' => $this->id,
], [
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
} else if ($variableName->startsWith('SERVICE_FQDN')) {
if ($fqdns) {
$number = Str::of($variableName)->after('SERVICE_FQDN')->afterLast('_')->value();
if (is_numeric($number)) {
$number = (int) $number - 1;
} else {
$number = 0;
}
$fqdn = getFqdnWithoutPort(data_get($fqdns, $number, $fqdns->first()));
$environments = collect(data_get($service, 'environment'));
$environments = $environments->map(function ($envValue) use ($value, $fqdn) {
$envValue = Str::of($envValue)->replace($value, $fqdn);
return $envValue->value();
});
$service['environment'] = $environments->toArray();
}
} else if ($variableName->startsWith('SERVICE_URL')) {
if ($fqdns) {
$number = Str::of($variableName)->after('SERVICE_URL')->afterLast('_')->value();
if (is_numeric($number)) {
$number = (int) $number - 1;
} else {
$number = 0;
}
$fqdn = getFqdnWithoutPort(data_get($fqdns, $number, $fqdns->first()));
$url = Url::fromString($fqdn)->getHost();
$environments = collect(data_get($service, 'environment'));
$environments = $environments->map(function ($envValue) use ($value, $url) {
$envValue = Str::of($envValue)->replace($value, $url);
return $envValue->value();
});
$service['environment'] = $environments->toArray();
}
}
}
}
if ($this->server->proxyType() === ProxyTypes::TRAEFIK_V2->value) {
$labels = collect(data_get($service, 'labels', []));
$labels = collect([]);
$labels = $labels->merge(defaultLabels($this->id, $container_name, type: 'service'));
if (!$isDatabase) {
if ($fqdns) {
$labels = $labels->merge(fqdnLabelsForTraefik($fqdns, $container_name, true));
}
}
data_set($service, 'labels', $labels->toArray());
}
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $container_name);
data_forget($service, 'documentation');
return $service;
});
$finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $composeVolumes->toArray(),
'networks' => $composeNetworks->toArray(),
];
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$shouldBeDefined = collect([
'envs' => $envs,
'volumes' => $volumes,
'ports' => $ports
]);
$parsedCompose = collect([
'dockerCompose' => $finalServices,
'shouldBeDefined' => $shouldBeDefined
]);
return $parsedCompose;
} else {
return collect([]);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceApplication extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function type()
{
return 'service';
}
public function documentation()
{
return data_get(Yaml::parse($this->service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs');
}
public function service()
{
return $this->belongsTo(Service::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceDatabase extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function type()
{
return 'service';
}
public function documentation()
{
return data_get(Yaml::parse($this->service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs');
}
public function service()
{
return $this->belongsTo(Service::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
}

View File

@@ -21,6 +21,11 @@ class StandaloneDocker extends BaseModel
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
} }
public function services()
{
return $this->morphMany(Service::class, 'destination');
}
public function attachedTo() public function attachedTo()
{ {
return $this->applications?->count() > 0 || $this->databases?->count() > 0; return $this->applications?->count() > 0 || $this->databases?->count() > 0;

View File

@@ -31,6 +31,7 @@ class StandalonePostgresql extends BaseModel
static::deleted(function ($database) { static::deleted(function ($database) {
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete();
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
}); });
} }

View File

@@ -14,7 +14,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public Application $application; public Application $application;
public ?ApplicationPreview $preview = null; public ?ApplicationPreview $preview = null;

View File

@@ -14,7 +14,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public Application $application; public Application $application;
public ApplicationPreview|null $preview = null; public ApplicationPreview|null $preview = null;

View File

@@ -13,7 +13,7 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public Application $application; public Application $application;
public string $application_name; public string $application_name;

View File

@@ -6,14 +6,16 @@ use Exception;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Log;
class EmailChannel class EmailChannel
{ {
public function send(SendsEmail $notifiable, Notification $notification): void public function send(SendsEmail $notifiable, Notification $notification): void
{ {
try {
$this->bootConfigs($notifiable); $this->bootConfigs($notifiable);
$recepients = $notifiable->getRecepients($notification); $recepients = $notifiable->getRecepients($notification);
ray($recepients);
if (count($recepients) === 0) { if (count($recepients) === 0) {
throw new Exception('No email recipients found'); throw new Exception('No email recipients found');
} }
@@ -27,6 +29,11 @@ class EmailChannel
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} catch (Exception $e) {
ray($e->getMessage());
send_internal_notification("EmailChannel error: {$e->getMessage()}. Failed to send email to: " . implode(', ', $recepients) . " with subject: {$mailMessage->subject}");
throw $e;
}
} }
private function bootConfigs($notifiable): void private function bootConfigs($notifiable): void

View File

@@ -12,7 +12,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null) public function __construct(public string $name, public Server $server, public ?string $url = null)

View File

@@ -3,8 +3,6 @@
namespace App\Notifications\Database; namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -14,7 +12,7 @@ class BackupFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public string $name; public string $name;
public string $frequency; public string $frequency;

View File

@@ -3,8 +3,6 @@
namespace App\Notifications\Database; namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -14,7 +12,7 @@ class BackupSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public string $name; public string $name;
public string $frequency; public string $frequency;

View File

@@ -25,7 +25,7 @@ class Textarea extends Component
public bool $readonly = false, public bool $readonly = false,
public string|null $helper = null, public string|null $helper = null,
public bool $realtimeValidation = false, 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"
) { ) {
// //
} }

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components\Services;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Explanation extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.services.explanation');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\View\Components\Services;
use App\Models\Service;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
use Illuminate\Support\Str;
class Links extends Component
{
public Collection $links;
public function __construct(public Service $service)
{
$this->links = collect([]);
$service->applications()->get()->map(function ($application) {
if ($application->fqdn) {
$fqdns = collect(Str::of($application->fqdn)->explode(','));
$fqdns->map(function ($fqdn) {
$this->links->push(getFqdnWithoutPort($fqdn));
});
}
if ($application->ports) {
$portsCollection = collect(Str::of($application->ports)->explode(','));
$portsCollection->map(function ($port) {
if (Str::of($port)->contains(':')) {
$hostPort = Str::of($port)->before(':');
} else {
$hostPort = $port;
}
$this->links->push(base_url(withPort:false) . ":{$hostPort}");
});
}
});
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.services.links');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\View\Components\Status;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Index extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public string $status = 'exited',
)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.status.index');
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\View\Components\Status;
use App\Models\Service;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Services extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public Service $service,
public string $complexStatus = 'exited',
) {
$this->complexStatus = serviceStatus($service);
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.status.services');
}
}

View File

@@ -10,3 +10,16 @@ const VALID_CRON_STRINGS = [
'yearly' => '0 0 1 1 *', 'yearly' => '0 0 1 1 *',
]; ];
const RESTART_MODE = 'unless-stopped'; const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [
'mysql',
'mariadb',
'postgres',
'mongo',
'redis',
'memcached',
'couchdb',
'neo4j',
'influxdb',
'clickhouse'
];

View File

@@ -1,11 +1,15 @@
<?php <?php
use App\Enums\ProxyTypes;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection { function getCurrentApplicationContainerStatus(Server $server, int $id): Collection
{
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
if (!$containers) { if (!$containers) {
return collect([]); return collect([]);
@@ -26,7 +30,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
} }
function format_docker_labels_to_json(string|Array $rawOutput): Collection function format_docker_labels_to_json(string|array $rawOutput): Collection
{ {
if (is_array($rawOutput)) { if (is_array($rawOutput)) {
return collect($rawOutput); return collect($rawOutput);
@@ -59,7 +63,8 @@ function format_docker_envs_to_json($rawOutput)
return collect([]); return collect([]);
} }
} }
function checkMinimumDockerEngineVersion($dockerVersion) { function checkMinimumDockerEngineVersion($dockerVersion)
{
$majorDockerVersion = Str::of($dockerVersion)->before('.')->value(); $majorDockerVersion = Str::of($dockerVersion)->before('.')->value();
if ($majorDockerVersion <= 22) { if ($majorDockerVersion <= 22) {
$dockerVersion = null; $dockerVersion = null;
@@ -72,7 +77,8 @@ function executeInDocker(string $containerId, string $command)
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'"; // return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
} }
function getApplicationContainerStatus(Application $application) { function getApplicationContainerStatus(Application $application)
{
$server = data_get($application, 'destination.server'); $server = data_get($application, 'destination.server');
$id = $application->id; $id = $application->id;
if (!$server) { if (!$server) {
@@ -98,13 +104,13 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return data_get($container[0], 'State.Status', 'exited'); return data_get($container[0], 'State.Status', 'exited');
} }
function generateApplicationContainerName(string $uuid, int $pull_request_id = 0) function generateApplicationContainerName(Application $application)
{ {
$now = now()->format('Hisu'); $now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($application->pull_request_id !== 0 && $application->pull_request_id !== null) {
return $uuid . '-pr-' . $pull_request_id; return $application->uuid . '-pr-' . $application->pull_request_id;
} else { } else {
return $uuid . '-' . $now; return $application->uuid . '-' . $now;
} }
} }
function get_port_from_dockerfile($dockerfile): int function get_port_from_dockerfile($dockerfile): int
@@ -123,3 +129,95 @@ function get_port_from_dockerfile($dockerfile): int
} }
return 80; return 80;
} }
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application')
{
$labels = collect([]);
$labels->push('coolify.managed=true');
$labels->push('coolify.version=' . config('version'));
$labels->push("coolify." . $type . "Id=" . $id);
$labels->push("coolify.type=$type");
$labels->push('coolify.name=' . $name);
if ($pull_request_id !== 0) {
$labels->push('coolify.pullRequestId=' . $pull_request_id);
}
return $labels;
}
function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
foreach ($domains as $domain) {
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
$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 ($port) {
$labels->push("traefik.http.routers.{$https_label}.service={$https_label}");
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
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 ($port) {
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
}
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);
$container_name = generateApplicationContainerName($application);
$appId = $application->id;
if ($pull_request_id !== 0) {
$appId = $appId . '-pr-' . $application->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(',');
} else {
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled));
}
return $labels->all();
}

View File

@@ -10,7 +10,22 @@ function get_proxy_path()
$proxy_path = "$base_path/proxy"; $proxy_path = "$base_path/proxy";
return $proxy_path; return $proxy_path;
} }
function connectProxyToNetworks(Server $server) {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
$commands = $networks->map(function ($network) {
return [
"echo '####### Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
];
});
return $commands->flatten();
}
function generate_default_proxy_configuration(Server $server) function generate_default_proxy_configuration(Server $server)
{ {
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
@@ -91,12 +106,11 @@ function generate_default_proxy_configuration(Server $server)
function setup_default_redirect_404(string|null $redirect_url, Server $server) function setup_default_redirect_404(string|null $redirect_url, Server $server)
{ {
ray('called');
$traefik_dynamic_conf_path = get_proxy_path() . "/dynamic"; $traefik_dynamic_conf_path = get_proxy_path() . "/dynamic";
$traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml"; $traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml";
ray($redirect_url);
if (empty($redirect_url)) { if (empty($redirect_url)) {
remote_process([ instant_remote_process([
"mkdir -p $traefik_dynamic_conf_path",
"rm -f $traefik_default_redirect_file", "rm -f $traefik_default_redirect_file",
], $server); ], $server);
} else { } else {
@@ -156,8 +170,7 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
$yaml; $yaml;
$base64 = base64_encode($yaml); $base64 = base64_encode($yaml);
ray("mkdir -p $traefik_dynamic_conf_path"); instant_remote_process([
remote_process([
"mkdir -p $traefik_dynamic_conf_path", "mkdir -p $traefik_dynamic_conf_path",
"echo '$base64' | base64 -d > $traefik_default_redirect_file", "echo '$base64' | base64 -d > $traefik_default_redirect_file",
], $server); ], $server);

View File

@@ -7,25 +7,28 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\NotReachable;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Activitylog\Contracts\Activity;
function remote_process( function remote_process(
array $command, Collection|array $command,
Server $server, Server $server,
string $type = ActivityTypes::INLINE->value, ?string $type = null,
?string $type_uuid = null, ?string $type_uuid = null,
?Model $model = null, ?Model $model = null,
bool $ignore_errors = false, bool $ignore_errors = false,
): Activity { ): Activity {
if (is_null($type)) {
$type = ActivityTypes::INLINE->value;
}
if ($command instanceof Collection) {
$command = $command->toArray();
}
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
if (auth()->user()) { if (auth()->user()) {
$teams = auth()->user()->teams->pluck('id'); $teams = auth()->user()->teams->pluck('id');
@@ -33,7 +36,6 @@ function remote_process(
throw new \Exception("User is not part of the team that owns this server"); throw new \Exception("User is not part of the team that owns this server");
} }
} }
return resolve(PrepareCoolifyTask::class, [ return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs( 'remoteProcessArgs' => new CoolifyTaskArgs(
server_uuid: $server->uuid, server_uuid: $server->uuid,
@@ -55,7 +57,7 @@ function remote_process(
// } // }
// // processWithEnv()->run("echo '{$server->privateKey->private_key}' | ssh-add -d -"); // // processWithEnv()->run("echo '{$server->privateKey->private_key}' | ssh-add -d -");
// } // }
function addPrivateKeyToSshAgent(Server $server) function savePrivateKeyToFs(Server $server)
{ {
if (data_get($server, 'privateKey.private_key') === null) { if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key"); throw new \Exception("Server {$server->name} does not have a private key");
@@ -72,7 +74,7 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
{ {
$user = $server->user; $user = $server->user;
$port = $server->port; $port = $server->port;
$privateKeyLocation = addPrivateKeyToSshAgent($server); $privateKeyLocation = savePrivateKeyToFs($server);
$timeout = config('constants.ssh.command_timeout'); $timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout'); $connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval'); $serverInterval = config('constants.ssh.server_interval');
@@ -83,6 +85,9 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
if ($isMux && config('coolify.mux_enabled')) { if ($isMux && config('coolify.mux_enabled')) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
} }
if (data_get($server,'settings.is_cloudflare_tunnel')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$privateKeyLocation} " $ssh_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
@@ -99,8 +104,11 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
// ray($ssh_command); // ray($ssh_command);
return $ssh_command; return $ssh_command;
} }
function instant_remote_process(array $command, Server $server, $throwError = true) function instant_remote_process(Collection|array $command, Server $server, $throwError = true)
{ {
if ($command instanceof Collection) {
$command = $command->toArray();
}
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$ssh_command = generateSshCommand($server, $command_string); $ssh_command = generateSshCommand($server, $command_string);
$process = Process::run($ssh_command); $process = Process::run($ssh_command);
@@ -110,11 +118,28 @@ function instant_remote_process(array $command, Server $server, $throwError = tr
if (!$throwError) { if (!$throwError) {
return null; return null;
} }
throw new \RuntimeException($process->errorOutput(), $exitCode); return excludeCertainErrors($process->errorOutput(), $exitCode);
} }
return $output; return $output;
} }
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) {
$ignoredErrors = collect([
'Permission denied (publickey',
'Could not resolve hostname',
]);
$ignored = false;
foreach ($ignoredErrors as $ignoredError) {
if (Str::contains($errorOutput, $ignoredError)) {
$ignored = true;
break;
}
}
if ($ignored) {
// TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorOutput, $exitCode);
}
throw new \RuntimeException($errorOutput, $exitCode);
}
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
{ {
$application = Application::find(data_get($application_deployment_queue, 'application_id')); $application = Application::find(data_get($application_deployment_queue, 'application_id'));
@@ -164,7 +189,7 @@ function validateServer(Server $server, bool $throwError = false)
]; ];
} }
$server->settings->is_reachable = true; $server->settings->is_reachable = true;
instant_remote_process(["docker ps"], $server, $throwError);
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError); $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError);
if (!$dockerVersion) { if (!$dockerVersion) {
$dockerVersion = null; $dockerVersion = null;

View File

@@ -0,0 +1,46 @@
<?php
use App\Models\Service;
use Illuminate\Support\Str;
function replaceRegex(?string $name = null)
{
return "/\\\${?{$name}[^}]*}?|\\\${$name}\w+/";
}
function collectRegex(string $name)
{
return "/{$name}\w+/";
}
function replaceVariables($variable)
{
return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
}
function serviceStatus(Service $service)
{
$foundRunning = false;
$isDegraded = false;
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else {
$isDegraded = true;
}
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && $isDegraded) {
return 'exited';
}
}

View File

@@ -20,24 +20,31 @@ use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
use Spatie\Url\Url;
function base_configuration_dir(): string
{
return '/data/coolify';
}
function application_configuration_dir(): string function application_configuration_dir(): string
{ {
return '/data/coolify/applications'; return base_configuration_dir() . "/applications";
}
function service_configuration_dir(): string
{
return base_configuration_dir() . "/services";
} }
function database_configuration_dir(): string function database_configuration_dir(): string
{ {
return '/data/coolify/databases'; return base_configuration_dir() . "/databases";
} }
function database_proxy_dir($uuid): string function database_proxy_dir($uuid): string
{ {
return "/data/coolify/databases/$uuid/proxy"; return base_configuration_dir() . "/databases/$uuid/proxy";
} }
function backup_dir(): string function backup_dir(): string
{ {
return '/data/coolify/backups'; return base_configuration_dir() . "/backups";
} }
function generate_readme_file(string $name, string $updated_at): string function generate_readme_file(string $name, string $updated_at): string
@@ -77,6 +84,7 @@ function refreshSession(?Team $team = null): void
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{ {
ray('handleError'); ray('handleError');
ray($error);
if ($error instanceof Throwable) { if ($error instanceof Throwable) {
$message = $error->getMessage(); $message = $error->getMessage();
} else { } else {
@@ -92,8 +100,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
} }
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->emit('error', $message); $livewire->emit('error', $message);
throw new RuntimeException($message);
} }
throw new RuntimeException($message); throw new RuntimeException($message);
} }
function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed
@@ -151,10 +161,12 @@ function get_latest_version_of_coolify(): string
} }
} }
function generate_random_name(): string function generate_random_name(?string $cuid = null): string
{ {
$generator = All::create(); $generator = All::create();
if (is_null($cuid)) {
$cuid = new Cuid2(7); $cuid = new Cuid2(7);
}
return Str::kebab("{$generator->getName()}-$cuid"); return Str::kebab("{$generator->getName()}-$cuid");
} }
function generateSSHKey() function generateSSHKey()
@@ -173,9 +185,11 @@ function formatPrivateKey(string $privateKey)
} }
return $privateKey; return $privateKey;
} }
function generate_application_name(string $git_repository, string $git_branch): string function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string
{ {
if (is_null($cuid)) {
$cuid = new Cuid2(7); $cuid = new Cuid2(7);
}
return Str::kebab("$git_repository:$git_branch-$cuid"); return Str::kebab("$git_repository:$git_branch-$cuid");
} }
@@ -227,7 +241,14 @@ function base_ip(): string
} }
return "localhost"; return "localhost";
} }
function getFqdnWithoutPort(String $fqdn)
{
$url = Url::fromString($fqdn);
$host = $url->getHost();
$scheme = $url->getScheme();
$path = $url->getPath();
return "$scheme://$host$path";
}
/** /**
* If fqdn is set, return it, otherwise return public ip. * If fqdn is set, return it, otherwise return public ip.
*/ */

View File

@@ -138,5 +138,6 @@ function allowedPathsForBoardingAccounts()
...allowedPathsForUnsubscribedAccounts(), ...allowedPathsForUnsubscribedAccounts(),
'boarding', 'boarding',
'livewire/message/boarding.index', 'livewire/message/boarding.index',
'livewire/message/activity-monitor'
]; ];
} }

View File

@@ -22,6 +22,7 @@
"lcobucci/jwt": "^5.0.0", "lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"livewire/livewire": "^v2.12.3", "livewire/livewire": "^v2.12.3",
"lorisleiva/laravel-actions": "^2.7",
"masmerise/livewire-toaster": "^1.2", "masmerise/livewire-toaster": "^1.2",
"nubs/random-name-generator": "^2.2", "nubs/random-name-generator": "^2.2",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",

151
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "cf138424c896f30b035bc8cdff63e8d1", "content-hash": "de2c45be3f03d43430549d963778dc4a",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -3059,6 +3059,153 @@
], ],
"time": "2023-08-11T04:02:34+00:00" "time": "2023-08-11T04:02:34+00:00"
}, },
{
"name": "lorisleiva/laravel-actions",
"version": "v2.7.1",
"source": {
"type": "git",
"url": "https://github.com/lorisleiva/laravel-actions.git",
"reference": "5250614fd6b77e8e2780be0206174e069e94661d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/5250614fd6b77e8e2780be0206174e069e94661d",
"reference": "5250614fd6b77e8e2780be0206174e069e94661d",
"shasum": ""
},
"require": {
"illuminate/contracts": "9.0 - 9.34 || ^9.36 || ^10.0",
"lorisleiva/lody": "^0.4",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^8.5",
"pestphp/pest": "^1.23",
"phpunit/phpunit": "^9.6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Lorisleiva\\Actions\\ActionServiceProvider"
],
"aliases": {
"Action": "Lorisleiva\\Actions\\Facades\\Actions"
}
}
},
"autoload": {
"psr-4": {
"Lorisleiva\\Actions\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Loris Leiva",
"email": "loris.leiva@gmail.com",
"homepage": "https://lorisleiva.com",
"role": "Developer"
}
],
"description": "Laravel components that take care of one specific task",
"homepage": "https://github.com/lorisleiva/laravel-actions",
"keywords": [
"action",
"command",
"component",
"controller",
"job",
"laravel",
"object"
],
"support": {
"issues": "https://github.com/lorisleiva/laravel-actions/issues",
"source": "https://github.com/lorisleiva/laravel-actions/tree/v2.7.1"
},
"funding": [
{
"url": "https://github.com/sponsors/lorisleiva",
"type": "github"
}
],
"time": "2023-08-24T10:20:57+00:00"
},
{
"name": "lorisleiva/lody",
"version": "v0.4.0",
"source": {
"type": "git",
"url": "https://github.com/lorisleiva/lody.git",
"reference": "1a43e8e423f3b2b64119542bc44a2071208fae16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lorisleiva/lody/zipball/1a43e8e423f3b2b64119542bc44a2071208fae16",
"reference": "1a43e8e423f3b2b64119542bc44a2071208fae16",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0|^9.0|^10.0",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^8.0",
"pestphp/pest": "^1.20.0",
"phpunit/phpunit": "^9.5.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Lorisleiva\\Lody\\LodyServiceProvider"
],
"aliases": {
"Lody": "Lorisleiva\\Lody\\Lody"
}
}
},
"autoload": {
"psr-4": {
"Lorisleiva\\Lody\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Loris Leiva",
"email": "loris.leiva@gmail.com",
"homepage": "https://lorisleiva.com",
"role": "Developer"
}
],
"description": "Load files and classes as lazy collections in Laravel.",
"homepage": "https://github.com/lorisleiva/lody",
"keywords": [
"classes",
"collection",
"files",
"laravel",
"load"
],
"support": {
"issues": "https://github.com/lorisleiva/lody/issues",
"source": "https://github.com/lorisleiva/lody/tree/v0.4.0"
},
"funding": [
{
"url": "https://github.com/sponsors/lorisleiva",
"type": "github"
}
],
"time": "2023-02-05T15:03:45+00:00"
},
{ {
"name": "masmerise/livewire-toaster", "name": "masmerise/livewire-toaster",
"version": "1.3.0", "version": "1.3.0",
@@ -13089,5 +13236,5 @@
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View File

@@ -15,7 +15,7 @@ return [
], ],
], ],
'limits' => [ 'limits' => [
'trial_period'=> 14, 'trial_period'=> 7,
'server' => [ 'server' => [
'zero' => 0, 'zero' => 0,
'self-hosted' => 999999999999, 'self-hosted' => 999999999999,

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.40', 'release' => '4.0.0-beta.45',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -30,7 +30,7 @@ return [
* *
* Minimum: 3000 (in milliseconds) * Minimum: 3000 (in milliseconds)
*/ */
'duration' => 1500, 'duration' => 3000,
/** /**
* The horizontal position of each toast. * The horizontal position of each toast.

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.40'; return '4.0.0-beta.45';

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->foreignId('server_id')->nullable();
$table->longText('description')->nullable();
$table->longText('docker_compose_raw');
$table->longText('docker_compose')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('server_id');
$table->dropColumn('description');
$table->dropColumn('docker_compose_raw');
$table->dropColumn('docker_compose');
});
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('service_databases', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('human_name')->nullable();
$table->longText('description')->nullable();
$table->longText('ports')->nullable();
$table->longText('exposes')->nullable();
$table->string('status')->default('exited');
$table->foreignId('service_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('service_databases');
}
};

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('service_applications', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('human_name')->nullable();
$table->longText('description')->nullable();
$table->string('fqdn')->unique()->nullable();
$table->longText('ports')->nullable();
$table->longText('exposes')->nullable();
$table->string('status')->default('exited');
$table->foreignId('service_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('service_applications');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('service_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('service_id');
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('local_file_volumes', function (Blueprint $table) {
$table->id();
$table->string('uuid');
$table->mediumText('fs_path');
$table->string('mount_path');
$table->mediumText('content')->nullable();
$table->nullableMorphs('resource');
$table->unique(['mount_path', 'resource_id', 'resource_type']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('local_file_volumes');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('is_cloudflare_tunnel')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('is_cloudflare_tunnel');
});
}
};

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class LocalFileVolumeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceApplicationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.30.0 ARG PACK_VERSION=0.30.0
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.14.0 ARG NIXPACKS_VERSION=1.16.0
USER root USER root
WORKDIR /artifacts WORKDIR /artifacts

View File

@@ -1,5 +1,9 @@
FROM serversideup/php:8.2-fpm-nginx FROM serversideup/php:8.2-fpm-nginx
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2023.8.2
ARG POSTGRES_VERSION=15 ARG POSTGRES_VERSION=15
RUN apt-get update RUN apt-get update
# Postgres version requirements # Postgres version requirements
@@ -13,15 +17,23 @@ RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements # Coolify requirements
RUN apt-get install -y php-pgsql openssh-client git git-lfs jq lsof RUN apt-get install -y php-pgsql openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev-ssu/nginx.conf /etc/nginx/conf.d/custom.conf COPY docker/dev-ssu/nginx.conf /etc/nginx/conf.d/custom.conf
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc
RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc
RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc
COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/ RUN mkdir -p /usr/local/bin
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"

View File

@@ -12,9 +12,14 @@ RUN npm install
RUN npm run build RUN npm run build
FROM serversideup/php:8.2-fpm-nginx FROM serversideup/php:8.2-fpm-nginx
WORKDIR /var/www/html
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2023.8.2
ARG POSTGRES_VERSION=15 ARG POSTGRES_VERSION=15
WORKDIR /var/www/html
RUN apt-get update RUN apt-get update
# Postgres version requirements # Postgres version requirements
RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
@@ -44,7 +49,16 @@ RUN php artisan view:cache
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc
RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc
RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc
RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"

View File

@@ -7,10 +7,6 @@ ARG DOCKER_VERSION=24.0.5
ARG DOCKER_COMPOSE_VERSION=2.21.0 ARG DOCKER_COMPOSE_VERSION=2.21.0
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.11.2 ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.30.0
# https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.14.0
USER root USER root
WORKDIR /root WORKDIR /root

View File

@@ -0,0 +1,51 @@
services:
ghost:
documentation: https://ghost.org/docs/config
image: ghost:5
volumes:
- ghost-content-data:/var/lib/ghost/content
- type: volume
source: /data/g
target: /data
volume:
nocopy: true
environment:
- url=$SERVICE_FQDN_GHOST
- database__client=mysql
- database__connection__host=mysql
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
- database__connection__database=${MYSQL_DATABASE-ghost}
networks:
default:
aliases:
- alias1
- alias3
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10
ports:
- "2368"
- 1234:2368
- target: 2368
published: 1234
protocol: tcp
mode: host
depends_on:
- mysql
mysql:
documentation: https://hub.docker.com/_/mysql
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
networks:
default:
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"
- subnet: "2001:3984:3989::/64"

View File

@@ -1,10 +1,11 @@
<div class="group"> <div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Links <label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down /> <x-chevron-down />
</label> </label>
<div class="absolute hidden group-hover:block"> <div class="absolute hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200"> <ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if (data_get($application, 'gitBrancLocation'))
<li> <li>
<a target="_blank" <a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
@@ -13,11 +14,12 @@
Git Repository Git Repository
</a> </a>
</li> </li>
@endif
@if (data_get($application, 'fqdn')) @if (data_get($application, 'fqdn'))
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn) @foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ $fqdn }}"> target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">
@@ -26,17 +28,17 @@
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path <path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $fqdn }} </svg>{{ getFqdnWithoutPort($fqdn) }}
</a> </a>
</li> </li>
@endforeach @endforeach
@endif @endif
@if (data_get($application, 'previews')->count() > 0) @if (data_get($application, 'previews', collect([]))->count() > 0)
@foreach (data_get($application, 'previews') as $preview) @foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn')) @if (data_get($preview, 'fqdn'))
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ data_get($preview, 'fqdn') }}"> target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">

View File

@@ -1,32 +1,12 @@
<div class="form-control"> <div class="form-control">
@if ($label) @if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium"> <label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
<span>
@if ($label)
{{ $label }}
@else
{{ $id }}
@endif
@if ($required) @if ($required)
<x-highlighted text="*" /> <x-highlighted text="*" />
@endif @endif
@if ($helper) @if ($helper)
<div class="group"> <x-helper :helper="$helper" />
<div class="cursor-pointer text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
{!! $helper !!}
</div>
</div>
</div>
@endif @endif
</span>
</label> </label>
@endif @endif
<textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }} <textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}

View File

@@ -1,7 +1,7 @@
@props([ @props([
'showSubscribeButtons' => true, 'showSubscribeButtons' => true,
]) ])
<div x-data="{ selected: 'yearly' }" class="w-full pb-20"> <div x-data="{ selected: 'monthly' }" class="w-full pb-20">
<div class="px-6 mx-auto lg:px-8"> <div class="px-6 mx-auto lg:px-8">
<div class="flex justify-center"> <div class="flex justify-center">
<fieldset <fieldset
@@ -24,80 +24,36 @@
<div class="py-2 text-center"><span class="font-bold text-warning">{{ config('constants.limits.trial_period') }} <div class="py-2 text-center"><span class="font-bold text-warning">{{ config('constants.limits.trial_period') }}
days trial</span> included on all plans, without credit card details.</div> days trial</span> included on all plans, without credit card details.</div>
<div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 "> <div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div>Save <span class="font-bold text-warning">1 month</span> annually with the yearly plans. <div>Save <span class="font-bold text-warning">10%</span> annually with the yearly plans.
</div> </div>
</div> </div>
<div x-show="selected === 'yearly'" class="flex justify-center h-10 mt-3 text-sm leading-6 "> <div x-show="selected === 'yearly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div> <div>
</div> </div>
</div> </div>
<div class="p-4 rounded bg-coolgray-400">
<h2 id="tier-hobby" class="flex items-start gap-4 text-4xl font-bold tracking-tight">Unlimited Trial
<x-forms.button><a class="font-bold text-white hover:no-underline"
href="https://github.com/coollabsio/coolify">Get Started</a></x-forms.button>
</h2>
<p class="mt-4 text-sm leading-6">Start self-hosting <span class="text-warning">without limits</span> with
our
OSS version. Same features as the paid version, but you have to manage by yourself.</p>
</div>
<div class="flow-root mt-12"> <div class="flow-root mt-12">
<div <div
class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-4 lg:divide-x lg:divide-y-0 xl:-mx-4"> class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap-y-16 sm:mx-auto lg:-mx-8 lg:mt-0 lg:max-w-none lg:grid-cols-3 lg:divide-x lg:divide-y-0 xl:-mx-4">
<div class="px-8 pt-16 lg:pt-0">
<h3 id="tier-trial" class="text-base font-semibold leading-7 text-white">Unlimited Trial</h3>
<p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">Free</span>
</span>
<span x-show="selected === 'yearly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">Still Free </span>
</span>
</p>
<span x-show="selected === 'monthly'" x-cloak>
<span>billed monthly</span>
</span>
<span x-show="selected === 'yearly'" x-cloak>
<span>billed annually</span>
</span>
<a href="https://github.com/coollabsio/coolify" aria-describedby="tier-trial" class="buyme">Get
Started</a>
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting without limits with our
OSS
version.</p>
<ul role="list" class="space-y-3 text-sm leading-6 ">
<li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
You manage everything
</li>
<li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Community Support
</li>
<li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3" />
<path d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0" />
</g>
</svg>
+ All upcoming features
</li>
</ul>
</div>
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14"> <div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
<h3 id="tier-basic" class="text-base font-semibold leading-7 text-white">Basic</h3> <h3 id="tier-basic" class="text-base font-semibold leading-7 text-white">Basic</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$5</span> <span class="text-4xl font-bold tracking-tight text-white">$5</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$4</span> <span class="text-4xl font-bold tracking-tight text-white">$4</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
</p> </p>
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
@@ -139,8 +95,8 @@
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600" <svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-linejoin="round" stroke-width="2"> stroke-width="2">
<path <path
d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3" /> d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3-5a9 9 0 0 0 6-8a3 3 0 0 0-3-3a9 9 0 0 0-8 6a6 6 0 0 0-5 3" />
<path d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0" /> <path d="M7 14a6 6 0 0 0-3 6a6 6 0 0 0 6-3m4-8a1 1 0 1 0 2 0a1 1 0 1 0-2 0" />
@@ -154,12 +110,12 @@
<h3 id="tier-pro" class="text-base font-semibold leading-7 text-white">Pro</h3> <h3 id="tier-pro" class="text-base font-semibold leading-7 text-white">Pro</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$29</span> <span class="text-4xl font-bold tracking-tight text-white">$30</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$26</span> <span class="text-4xl font-bold tracking-tight text-white">$27</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
</p> </p>
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
@@ -192,7 +148,7 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
Basic Support Included Email System
</li> </li>
<li class="flex gap-x-3"> <li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
@@ -201,7 +157,7 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
Included Email System Email Support
</li> </li>
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600" <svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
@@ -221,12 +177,12 @@
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3> <h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3>
<p class="flex items-baseline mt-6 gap-x-1"> <p class="flex items-baseline mt-6 gap-x-1">
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$69</span> <span class="text-4xl font-bold tracking-tight text-white">$?</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
<span x-show="selected === 'yearly'" x-cloak> <span x-show="selected === 'yearly'" x-cloak>
<span class="text-4xl font-bold tracking-tight text-white">$63</span> <span class="text-4xl font-bold tracking-tight text-white">$?</span>
<span class="text-sm font-semibold leading-6 ">/month</span> <span class="text-sm font-semibold leading-6 ">/month + VAT</span>
</span> </span>
</p> </p>
<span x-show="selected === 'monthly'" x-cloak> <span x-show="selected === 'monthly'" x-cloak>
@@ -250,17 +206,9 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
25 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." /> ? servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
</li>
<li class="flex font-bold text-white gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Priority Support
</li> </li>
<li class="flex gap-x-3"> <li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true"> aria-hidden="true">
@@ -270,6 +218,15 @@
</svg> </svg>
Included Email System Included Email System
</li> </li>
<li class="flex font-bold text-white gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Priority (Email/Chat) Support
</li>
<li class="flex font-bold text-white gap-x-3"> <li class="flex font-bold text-white gap-x-3">
<svg width="512" height="512" class="flex-none w-5 h-6 text-green-600" <svg width="512" height="512" class="flex-none w-5 h-6 text-green-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -285,8 +242,13 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="pt-10">Need unlimited servers or official support for your Coolify instance? <a <div class="p-4 mt-10 rounded">
href="https://docs.coollabs.io/contact" class='text-warning'>Contact us.</a> <div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance?
<x-forms.button>
<a class="font-bold text-white hover:no-underline"
href="https://docs.coollabs.io/contact">Contact Us</a>
</x-forms.button>
</div> </div>
</div> </div>
</div> </div>
@@ -297,8 +259,8 @@
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500"> <div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24" <svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-width="2" stroke-linejoin="round" stroke-width="2"
d="M3 7a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm12 13H6a3 3 0 0 1-3-3v-2a3 3 0 0 1 3-3h12M7 8v.01M7 16v.01M20 15l-2 3h3l-2 3" /> d="M3 7a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm12 13H6a3 3 0 0 1-3-3v-2a3 3 0 0 1 3-3h12M7 8v.01M7 16v.01M20 15l-2 3h3l-2 3" />
</svg> </svg>
</div> </div>
@@ -339,8 +301,10 @@
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"> stroke-width="2">
<path d="M15 11h2a2 2 0 0 1 2 2v2m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h4" /> <path
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0m-3-5V8m.347-3.631A4 4 0 0 1 16 6M3 3l18 18" /> d="M15 11h2a2 2 0 0 1 2 2v2m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h4" />
<path
d="M11 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0m-3-5V8m.347-3.631A4 4 0 0 1 16 6M3 3l18 18" />
</g> </g>
</svg> </svg>
</div> </div>
@@ -356,8 +320,9 @@
<div> <div>
<div class="flex items-center gap-4 mb-4"> <div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500"> <div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="3" y="4" width="18" height="12" rx="1" /> <rect x="3" y="4" width="18" height="12" rx="1" />
<path d="M7 20h10" /> <path d="M7 20h10" />
@@ -396,8 +361,9 @@
<div> <div>
<div class="flex items-center gap-4 mb-4"> <div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500"> <div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="5 7 10 12 5 17" /> <polyline points="5 7 10 12 5 17" />
<line x1="13" y1="17" x2="19" y2="17" /> <line x1="13" y1="17" x2="19" y2="17" />
@@ -429,7 +395,8 @@
</div> </div>
<div class="mt-1 text-base leading-7 text-gray-300"> <div class="mt-1 text-base leading-7 text-gray-300">
Git integration is default today. We support hosted (github.com, Git integration is default today. We support hosted (github.com,
gitlab.com<span class="inline-block text-warning">*</span>) or self-hosted<span class="text-warning">*</span> gitlab.com<span class="inline-block text-warning">*</span>) or self-hosted<span
class="text-warning">*</span>
(Github Enterprise, Gitlab) Git repositories. (Github Enterprise, Gitlab) Git repositories.
</div> </div>
</div> </div>
@@ -438,8 +405,8 @@
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500"> <div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24" <svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-width="2" stroke-linejoin="round" stroke-width="2"
d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1M15 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m2 5h2a2 2 0 0 1 2 2v1M5 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h2" /> d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1M15 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m2 5h2a2 2 0 0 1 2 2v1M5 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h2" />
</svg> </svg>
</div> </div>

View File

@@ -38,12 +38,10 @@
</svg> </svg>
</div> </div>
</li> </li>
@if ($resource->status === 'running') @if ($resource->getMorphClass() == 'App\Models\Service')
<x-status.running /> <x-status.services :service="$resource" />
@elseif($resource->status === 'restarting')
<x-status.restarting />
@else @else
<x-status.stopped /> <x-status.index :status="$resource->status" />
@endif @endif
</ol> </ol>
</nav> </nav>

View File

@@ -2,9 +2,7 @@
<livewire:server.proxy.modal :server="$server" /> <livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
@if ($server->settings->is_reachable)
<livewire:server.proxy.status :server="$server" /> <livewire:server.proxy.status :server="$server" />
@endif
</div> </div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div> <div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main"> <nav class="navbar-main">

View File

@@ -0,0 +1,28 @@
<div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
<div class="absolute hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if ($links->count() > 0)
@foreach ($links as $link)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ $link }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $link }}
</a>
</li>
@endforeach
@endif
</ul>
</div>
</div>

View File

@@ -0,0 +1,45 @@
<div class="navbar-main">
<x-services.links :service="$service" />
<div class="flex-1"></div>
@if (serviceStatus($service) === 'running')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@elseif(serviceStatus($service) === 'exited')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@elseif (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Degraded Services
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@endif
</div>

Some files were not shown because too many files have changed in this diff Show More