Compare commits

...

11 Commits

Author SHA1 Message Date
Andras Bacsai
df021760a7 Merge pull request #1423 from coollabsio/next
v4.0.0-beta.120
2023-11-10 12:06:55 +01:00
Andras Bacsai
fb2598f2e4 Update UI elements and add new build pack option (static) 2023-11-10 11:33:15 +01:00
Andras Bacsai
7af07b2718 Add logging to DockerCleanupJob 2023-11-10 10:55:23 +01:00
Andras Bacsai
23a94c9378 Refactor DockerCleanupJob and Application model 2023-11-10 10:34:28 +01:00
Andras Bacsai
ed34fc9645 Update defaultClass in Select component 2023-11-10 10:14:46 +01:00
Andras Bacsai
cafd9e0ab2 Convert cpus limits to integer in database and
application classes
2023-11-10 09:54:40 +01:00
Andras Bacsai
e882477e21 Refactor navbar and add help us link 2023-11-10 09:49:47 +01:00
Andras Bacsai
db0e3cfcc4 fix: database proxy for services
version++
tiny css modifications
2023-11-10 09:41:42 +01:00
Andras Bacsai
b3c4429028 Merge pull request #1422 from coollabsio/next
v4.0.0-beta.119
2023-11-09 15:10:56 +01:00
Andras Bacsai
87ab4bd71e fix: local ip address 2023-11-09 15:05:42 +01:00
Andras Bacsai
61e1fdede9 feat: make service databases public 2023-11-09 14:59:38 +01:00
29 changed files with 284 additions and 68 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@@ -14,21 +15,53 @@ class StartDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{ {
$internalPort = null; $internalPort = null;
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { $type = $database->getMorphClass();
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$databaseType = $database->databaseType();
$network = data_get($database, 'service.destination.network');
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
$type = 'App\Models\StandaloneMariadb';
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = 'App\Models\StandaloneMongodb';
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = 'App\Models\StandaloneMysql';
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = 'App\Models\StandalonePostgresql';
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = 'App\Models\StandaloneRedis';
$containerName = "redis-{$database->service->uuid}";
break;
}
}
if ($type === 'App\Models\StandaloneRedis') {
$internalPort = 6379; $internalPort = 6379;
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') { } else if ($type === 'App\Models\StandalonePostgresql') {
$internalPort = 5432; $internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') { } else if ($type === 'App\Models\StandaloneMongodb') {
$internalPort = 27017; $internalPort = 27017;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') { } else if ($type === 'App\Models\StandaloneMysql') {
$internalPort = 3306; $internalPort = 3306;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') { } else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306; $internalPort = 3306;
} }
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF $nginxconf = <<<EOF
user nginx; user nginx;
@@ -42,7 +75,7 @@ class StartDatabaseProxy
stream { stream {
server { server {
listen $database->public_port; listen $database->public_port;
proxy_pass $database->uuid:$internalPort; proxy_pass $containerName:$internalPort;
} }
} }
EOF; EOF;
@@ -54,19 +87,19 @@ class StartDatabaseProxy
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
$containerName => [ $proxyContainerName => [
'build' => [ 'build' => [
'context' => $configuration_dir, 'context' => $configuration_dir,
'dockerfile' => 'Dockerfile', 'dockerfile' => 'Dockerfile',
], ],
'image' => "nginx:stable-alpine", 'image' => "nginx:stable-alpine",
'container_name' => $containerName, 'container_name' => $proxyContainerName,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'ports' => [ 'ports' => [
"$database->public_port:$database->public_port", "$database->public_port:$database->public_port",
], ],
'networks' => [ 'networks' => [
$database->destination->network, $network,
], ],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
@@ -81,9 +114,9 @@ class StartDatabaseProxy
] ]
], ],
'networks' => [ 'networks' => [
$database->destination->network => [ $network => [
'external' => true, 'external' => true,
'name' => $database->destination->network, 'name' => $network,
'attachable' => true, 'attachable' => true,
] ]
] ]
@@ -97,6 +130,6 @@ class StartDatabaseProxy
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf", "echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml", "echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up --build -d", "docker compose --project-directory {$configuration_dir} up --build -d",
], $database->destination->server); ], $server);
} }
} }

View File

@@ -56,7 +56,7 @@ class StartMariadb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -56,7 +56,7 @@ class StartMysql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -66,7 +66,7 @@ class StartPostgresql
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -65,7 +65,7 @@ class StartRedis
'memswap_limit' => $this->database->limits_memory_swap, 'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness, 'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation, 'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus, 'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset, 'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares, 'cpu_shares' => $this->database->limits_cpu_shares,
] ]

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
@@ -13,9 +14,13 @@ class StopDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{ {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); $server = data_get($database, 'destination.server');
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $server);
$database->is_public = false; $database->is_public = false;
$database->save(); $database->save();
} }

View File

@@ -2,28 +2,56 @@
namespace App\Http\Livewire\Project\Service; namespace App\Http\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Livewire\Component; use Livewire\Component;
class Database extends Component class Database extends Component
{ {
public ServiceDatabase $database; public ServiceDatabase $database;
public ?string $db_url_public = null;
public $fileStorages; public $fileStorages;
protected $listeners = ["refreshFileStorages"]; protected $listeners = ["refreshFileStorages"];
protected $rules = [ protected $rules = [
'database.human_name' => 'nullable', 'database.human_name' => 'nullable',
'database.description' => 'nullable', 'database.description' => 'nullable',
'database.image' => 'required', 'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean', 'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean',
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.database'); return view('livewire.project.service.database');
} }
public function mount() { public function mount() {
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages(); $this->refreshFileStorages();
} }
public function instantSave() { public function instantSave() {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getServiceDatabaseUrl();
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->submit(); $this->submit();
} }
public function refreshFileStorages() public function refreshFileStorages()

View File

@@ -192,6 +192,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->deploy_dockerimage_buildpack(); $this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') { } else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack(); $this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else { } else {
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
@@ -227,6 +229,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[ [
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
"hidden" => true, "hidden" => true,
"ignore_errors" => true,
]
);
$this->execute_remote_command(
[
"docker image prune -f >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
] ]
); );
} }
@@ -421,6 +431,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image(); $this->build_image();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_static_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
$this->cleanup_git();
$this->build_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function rolling_update() private function rolling_update()
{ {
@@ -787,7 +814,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap, 'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness, 'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation, 'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => $this->application->limits_cpus, 'cpus' => (int) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset, 'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares, 'cpu_shares' => $this->application->limits_cpu_shares,
] ]
@@ -910,22 +937,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function build_image() private function build_image()
{ {
$this->execute_remote_command([ if ($this->application->build_pack === 'static') {
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
if ($this->application->settings->is_static) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true "echo -n 'Static deployment. Copying static assets to the image.'",
]); ]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
}
$dockerfile = base64_encode("FROM {$this->application->static_image} if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/ WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid} LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
$nginx_config = base64_encode("server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name localhost; server_name localhost;
@@ -941,15 +972,43 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
root /usr/share/nginx/html; root /usr/share/nginx/html;
} }
}"); }");
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}");
}
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile-prod") executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
], ],
[ [
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
], ],
[ [
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
] ]
); );
} else { } else {

View File

@@ -3,6 +3,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Server; use App\Models\Server;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -10,12 +11,13 @@ 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\Facades\Log;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000; public $timeout = 300;
public ?string $dockerRootFilesystem = null; public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null; public ?int $usageBefore = null;
@@ -33,14 +35,15 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
} }
public function handle(): void public function handle(): void
{ {
$queuedCount = 0; $isInprogress = false;
$this->server->applications()->each(function ($application) use ($queuedCount) { $this->server->applications()->each(function ($application) use (&$isInprogress) {
$count = data_get($application->deployments(), 'count', 0); if ($application->isDeploymentInprogress()) {
$queuedCount += $count; $isInprogress = true;
return;
}
}); });
if ($queuedCount > 0) { if ($isInprogress) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange'); throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
return;
} }
try { try {
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
@@ -49,23 +52,25 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerRootFilesystem = "/"; $this->dockerRootFilesystem = "/";
$this->usageBefore = $this->getFilesystemUsage(); $this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange'); ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server); instant_remote_process(['docker image prune -af'], $this->server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
instant_remote_process(['docker builder prune -af'], $this->server); instant_remote_process(['docker builder prune -af'], $this->server);
$usageAfter = $this->getFilesystemUsage(); $usageAfter = $this->getFilesystemUsage();
if ($usageAfter < $this->usageBefore) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange'); ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else { } else {
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange'); Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);
} }
} else { } else {
ray('No need to clean up ' . $this->server->name)->color('orange'); ray('No need to clean up ' . $this->server->name);
Log::info('No need to clean up ' . $this->server->name);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage())->color('orange'); ray($e->getMessage());
throw $e; throw $e;
} }
} }

View File

@@ -213,6 +213,14 @@ class Application extends BaseModel
return $this->morphTo(); return $this->morphTo();
} }
public function isDeploymentInprogress() {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
}
return false;
}
public function deployments(int $skip = 0, int $take = 10) public function deployments(int $skip = 0, int $take = 10)
{ {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');

View File

@@ -28,6 +28,16 @@ class ServiceDatabase extends BaseModel
} }
return "standalone-$image"; return "standalone-$image";
} }
public function getServiceDatabaseUrl() {
// $type = $this->databaseType();
$port = $this->public_port;
$realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) {
$realIp = base_ip();
}
$url = "{$realIp}:{$port}";
return $url;
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);

View File

@@ -58,8 +58,9 @@ class StandaloneRedis extends BaseModel
{ {
return 'standalone-redis'; return 'standalone-redis';
} }
public function getDbUrl(): string { public function getDbUrl(bool $useInternal = false): string
if ($this->is_public) { {
if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else { } else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";

View File

@@ -20,7 +20,7 @@ class Input extends Component
public bool $readonly = false, public bool $readonly = false,
public string|null $helper = null, public string|null $helper = null,
public bool $allowToPeak = true, public bool $allowToPeak = true,
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full 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 = "input input-sm bg-coolgray-100 rounded text-white w-full 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

@@ -19,7 +19,7 @@ class Select extends Component
public string|null $label = null, public string|null $label = null,
public string|null $helper = null, public string|null $helper = null,
public bool $required = false, public bool $required = false,
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-200 font-normal disabled:bg-coolgray-200/50 disabled:border-none" public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
) { ) {
// //
} }

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 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" public string $defaultClass = "textarea leading-normal bg-coolgray-100 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

@@ -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.118', 'release' => '4.0.0-beta.120',
// 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

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.118'; return '4.0.0-beta.120';

View File

@@ -0,0 +1,30 @@
<?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('service_databases', function (Blueprint $table) {
$table->integer('public_port')->nullable();
$table->boolean('is_public')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('public_port');
$table->dropColumn('is_public');
});
}
};

View File

@@ -53,7 +53,7 @@ a {
@apply text-white; @apply text-white;
} }
.box { .box {
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
} }
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];

View File

@@ -1,7 +1,7 @@
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div> <div>
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200" <div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-100"
@click="showCommandPalette = true"> @click="showCommandPalette = true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -1,6 +1,7 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> <a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -11,6 +12,18 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" 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.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,6 +1,7 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> <a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -87,6 +88,18 @@
@if (isInstanceAdmin() && !isCloud()) @if (isInstanceAdmin() && !isCloud())
<livewire:upgrade /> <livewire:upgrade />
@endif @endif
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" 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.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Profile"> <li title="Profile">
<a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif> <a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif>
<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-width="1.5"

View File

@@ -25,7 +25,6 @@
@endif @endif
</head> </head>
@section('body') @section('body')
<body> <body>
@livewireScripts @livewireScripts
<dialog id="help" class="modal"> <dialog id="help" class="modal">

View File

@@ -27,10 +27,11 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select wire:model="application.build_pack" label="Build Pack" required> <x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option> <option value="dockerimage">Docker Image</option>
</x-forms.select> </x-forms.select>
@if ($application->settings->is_static) @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required> <x-forms.select id="application.static_image" label="Static Image" required>
<option value="nginx:alpine">nginx:alpine</option> <option value="nginx:alpine">nginx:alpine</option>
<option disabled value="apache:alpine">apache:alpine</option> <option disabled value="apache:alpine">apache:alpine</option>
@@ -97,7 +98,7 @@
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly /> <x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else @else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required <x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required

View File

@@ -12,10 +12,19 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input> <x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
<x-forms.input label="Description" id="database.description"></x-forms.input> <x-forms.input label="Description" id="database.description"></x-forms.input>
<x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image Tag" id="database.image"></x-forms.input>
</div> </div>
<div class="flex gap-2"> <div class="flex items-end gap-2">
<x-forms.input required helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag" @if ($db_url_public)
id="database.image"></x-forms.input> <x-forms.input label="Database URL (public)"
helper="Your credentials are available in your environment variables." type="password" readonly
wire:model="db_url_public" />
@endif
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
</div> </div>
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>

View File

@@ -2,7 +2,9 @@
@if ( @if (
$resource->getMorphClass() == 'App\Models\Application' || $resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql' || $resource->getMorphClass() == 'App\Models\StandalonePostgresql' ||
$resource->getMorphClass() == 'App\Models\StandaloneRedis') $resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
$resource->getMorphClass() == 'App\Models\StandaloneMariadb' ||
$resource->getMorphClass() == 'App\Models\StandaloneMongodb')
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Storages</h2> <h2>Storages</h2>
<x-helper <x-helper

View File

@@ -39,12 +39,12 @@ module.exports = {
themes: [ themes: [
{ {
coollabs: { coollabs: {
primary: "#323232", primary: "#202020",
"primary-focus": "#242424", "primary-focus": "#242424",
secondary: "#6B16ED", secondary: "#6B16ED",
accent: "#4338ca", accent: "#4338ca",
neutral: "#1B1D1D", neutral: "#1B1D1D",
"base-100": "#181818", "base-100": "#101010",
info: "#2563EB", info: "#2563EB",
success: "#16A34A", success: "#16A34A",
warning: "#FCD34D", warning: "#FCD34D",

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.118" "version": "4.0.0-beta.120"
} }
} }
} }