Merge branch 'next' into main

This commit is contained in:
Eirik Mo 2024-03-14 19:21:20 +01:00 committed by GitHub
commit 11cd553949
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
134 changed files with 2042 additions and 720 deletions

View File

@ -39,7 +39,7 @@ class PrepareCoolifyTask
public function __invoke(): Activity public function __invoke(): Activity
{ {
$job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish); $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data);
dispatch($job); dispatch($job);
$this->activity->refresh(); $this->activity->refresh();
return $this->activity; return $this->activity;

View File

@ -21,6 +21,8 @@ class RunRemoteProcess
public $call_event_on_finish = null; public $call_event_on_finish = null;
public $call_event_data = null;
protected $time_start; protected $time_start;
protected $current_time; protected $current_time;
@ -34,7 +36,7 @@ class RunRemoteProcess
/** /**
* Create a new job instance. * Create a new job instance.
*/ */
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null) public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null)
{ {
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) { if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) {
@ -45,6 +47,7 @@ class RunRemoteProcess
$this->hide_from_output = $hide_from_output; $this->hide_from_output = $hide_from_output;
$this->ignore_errors = $ignore_errors; $this->ignore_errors = $ignore_errors;
$this->call_event_on_finish = $call_event_on_finish; $this->call_event_on_finish = $call_event_on_finish;
$this->call_event_data = $call_event_data;
} }
public static function decodeOutput(?Activity $activity = null): string public static function decodeOutput(?Activity $activity = null): string
@ -111,9 +114,15 @@ class RunRemoteProcess
} }
if ($this->call_event_on_finish) { if ($this->call_event_on_finish) {
try { try {
event(resolve("App\\Events\\$this->call_event_on_finish", [ if ($this->call_event_data) {
'userId' => $this->activity->causer_id, event(resolve("App\\Events\\$this->call_event_on_finish", [
])); "data" => $this->call_event_data,
]));
} else {
event(resolve("App\\Events\\$this->call_event_on_finish", [
'userId' => $this->activity->causer_id,
]));
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
} }

View File

@ -11,7 +11,12 @@ class CheckConfiguration
use AsAction; use AsAction;
public function handle(Server $server, bool $reset = false) public function handle(Server $server, bool $reset = false)
{ {
$proxy_path = get_proxy_path(); $proxyType = $server->proxyType();
if ($proxyType === 'NONE') {
return 'OK';
}
$proxy_path = $server->proxyPath();
$proxy_configuration = instant_remote_process([ $proxy_configuration = instant_remote_process([
"mkdir -p $proxy_path", "mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml", "cat $proxy_path/docker-compose.yml",

View File

@ -10,6 +10,9 @@ class CheckProxy
use AsAction; use AsAction;
public function handle(Server $server, $fromUI = false) public function handle(Server $server, $fromUI = false)
{ {
if ($server->proxyType() === 'NONE') {
return false;
}
if (!$server->isProxyShouldRun()) { if (!$server->isProxyShouldRun()) {
if ($fromUI) { if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy."); throw new \Exception("Proxy should not run. You selected the Custom Proxy.");

View File

@ -15,7 +15,7 @@ class SaveConfiguration
if (is_null($proxy_settings)) { if (is_null($proxy_settings)) {
$proxy_settings = CheckConfiguration::run($server, true); $proxy_settings = CheckConfiguration::run($server, true);
} }
$proxy_path = get_proxy_path(); $proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($proxy_settings); $docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;

View File

@ -2,7 +2,7 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Events\ProxyStatusChanged; use App\Events\ProxyStarted;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -15,11 +15,11 @@ class StartProxy
{ {
try { try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
if ($proxyType === 'NONE') { if (is_null($proxyType) || $proxyType === 'NONE') {
return 'OK'; return 'OK';
} }
$commands = collect([]); $commands = collect([]);
$proxy_path = get_proxy_path(); $proxy_path = $server->proxyPath();
$configuration = CheckConfiguration::run($server); $configuration = CheckConfiguration::run($server);
if (!$configuration) { if (!$configuration) {
throw new \Exception("Configuration is not synced"); throw new \Exception("Configuration is not synced");
@ -37,8 +37,10 @@ class StartProxy
"echo 'Proxy started successfully.'" "echo 'Proxy started successfully.'"
]); ]);
} else { } else {
$caddfile = "import /dynamic/*.caddy";
$commands = $commands->merge([ $commands = $commands->merge([
"mkdir -p $proxy_path/dynamic && cd $proxy_path", "mkdir -p $proxy_path/dynamic && cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'", "echo 'Pulling docker image.'",
'docker compose pull', 'docker compose pull',
@ -52,13 +54,14 @@ class StartProxy
} }
if ($async) { if ($async) {
$activity = remote_process($commands, $server); $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
return $activity; return $activity;
} else { } else {
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
$server->proxy->set('status', 'running'); $server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType); $server->proxy->set('type', $proxyType);
$server->save(); $server->save();
ProxyStarted::dispatch($server);
return 'OK'; return 'OK';
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@ -12,9 +12,13 @@ class CleanupDatabase extends Command
public function handle() public function handle()
{ {
echo "Running database cleanup...\n"; if ($this->option('yes')) {
echo "Running database cleanup...\n";
} else {
echo "Running database cleanup in dry-run mode...\n";
}
$keep_days = 60; $keep_days = 60;
echo "Keep days: $keep_days\n";
// Cleanup failed jobs table // Cleanup failed jobs table
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7)); $failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7));
$count = $failed_jobs->count(); $count = $failed_jobs->count();
@ -32,7 +36,7 @@ class CleanupDatabase extends Command
} }
// Cleanup activity_log table // Cleanup activity_log table
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days)); $activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10);
$count = $activity_log->count(); $count = $activity_log->count();
echo "Delete $count entries from activity_log.\n"; echo "Delete $count entries from activity_log.\n";
if ($this->option('yes')) { if ($this->option('yes')) {
@ -40,7 +44,7 @@ class CleanupDatabase extends Command
} }
// Cleanup application_deployment_queues table // Cleanup application_deployment_queues table
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days)); $application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10);
$count = $application_deployment_queues->count(); $count = $application_deployment_queues->count();
echo "Delete $count entries from application_deployment_queues.\n"; echo "Delete $count entries from application_deployment_queues.\n";
if ($this->option('yes')) { if ($this->option('yes')) {

View File

@ -35,7 +35,8 @@ class Init extends Command
$this->call('cleanup:queue'); $this->call('cleanup:queue');
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
try { try {
setup_dynamic_configuration(); $server = Server::find(0)->first();
$server->setupDynamicProxyConfiguration();
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} }

View File

@ -100,6 +100,12 @@ class ServicesGenerate extends Command
} else { } else {
$tags = null; $tags = null;
} }
$port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values();
if ($port->count() > 0) {
$port = str($port[0])->after('# port:')->trim()->value();
} else {
$port = null;
}
$json = Yaml::parse($content); $json = Yaml::parse($content);
$yaml = base64_encode(Yaml::dump($json, 10, 2)); $yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [ $payload = [
@ -111,6 +117,9 @@ class ServicesGenerate extends Command
'logo' => $logo, 'logo' => $logo,
'minversion' => $minversion, 'minversion' => $minversion,
]; ];
if ($port) {
$payload['port'] = $port;
}
if ($env_file) { if ($env_file) {
$env_file_content = file_get_contents(base_path("templates/compose/$env_file")); $env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
$env_file_base64 = base64_encode($env_file_content); $env_file_base64 = base64_encode($env_file_content);

View File

@ -21,6 +21,7 @@ class CoolifyTaskArgs extends Data
public ?string $status = null , public ?string $status = null ,
public bool $ignore_errors = false, public bool $ignore_errors = false,
public $call_event_on_finish = null, public $call_event_on_finish = null,
public $call_event_data = null
) { ) {
if(is_null($status)){ if(is_null($status)){
$this->status = ProcessStatus::QUEUED->value; $this->status = ProcessStatus::QUEUED->value;

View File

@ -0,0 +1,16 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStarted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public $data)
{
}
}

View File

@ -9,13 +9,33 @@ use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis; use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService; use App\Actions\Service\StartService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag; use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Deploy extends Controller class Deploy extends Controller
{ {
public function deployments(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = Server::whereTeamId($teamId)->get();
$deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $servers->pluck("id"))->get([
"id",
"application_id",
"application_name",
"deployment_url",
"pull_request_id",
"server_name",
"server_id",
"status"
])->sortBy('id')->toArray();
return response()->json($deployments_per_server, 200);
}
public function deploy(Request $request) public function deploy(Request $request)
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
@ -27,7 +47,7 @@ class Deploy extends Controller
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
} }
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
if ($tags) { if ($tags) {
return $this->by_tags($tags, $teamId, $force); return $this->by_tags($tags, $teamId, $force);
@ -44,16 +64,22 @@ class Deploy extends Controller
if (count($uuids) === 0) { if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
} }
$message = collect([]); $deployments = collect();
$payload = collect();
foreach ($uuids as $uuid) { foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId); $resource = getResourceByUuid($uuid, $teamId);
if ($resource) { if ($resource) {
$return_message = $this->deploy_resource($resource, $force); ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message); if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
}
} }
} }
if ($message->count() > 0) { if ($deployments->count() > 0) {
return response()->json(['message' => $message->toArray()], 200); $payload->put('deployments', $deployments->toArray());
return response()->json($payload->toArray(), 200);
} }
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404); return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
} }
@ -66,10 +92,12 @@ class Deploy extends Controller
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
} }
$message = collect([]); $message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) { foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (!$found_tag) { if (!$found_tag) {
$message->push("Tag {$tag} not found."); // $message->push("Tag {$tag} not found.");
continue; continue;
} }
$applications = $found_tag->applications()->get(); $applications = $found_tag->applications()->get();
@ -79,83 +107,78 @@ class Deploy extends Controller
continue; continue;
} }
foreach ($applications as $resource) { foreach ($applications as $resource) {
$return_message = $this->deploy_resource($resource, $force); ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
}
$message = $message->merge($return_message); $message = $message->merge($return_message);
} }
foreach ($services as $resource) { foreach ($services as $resource) {
$return_message = $this->deploy_resource($resource, $force); ['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message); $message = $message->merge($return_message);
} }
} }
ray($message);
if ($message->count() > 0) { if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200); $payload->put('message', $message->toArray());
if ($deployments->count() > 0) {
$payload->put('details', $deployments->toArray());
}
return response()->json($payload->toArray(), 200);
} }
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404); return response()->json(['error' => "No resources found with this tag.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
} }
public function deploy_resource($resource, bool $force = false): Collection public function deploy_resource($resource, bool $force = false): array
{ {
$message = collect([]); $message = null;
$deployment_uuid = null;
if (gettype($resource) !== 'object') { if (gettype($resource) !== 'object') {
return $message->push("Resource ($resource) not found."); return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
} }
$type = $resource?->getMorphClass(); $type = $resource?->getMorphClass();
if ($type === 'App\Models\Application') { if ($type === 'App\Models\Application') {
$deployment_uuid = new Cuid2(7);
queue_application_deployment( queue_application_deployment(
application: $resource, application: $resource,
deployment_uuid: new Cuid2(7), deployment_uuid: $deployment_uuid,
force_rebuild: $force, force_rebuild: $force,
); );
$message->push("Application {$resource->name} deployment queued."); $message = "Application {$resource->name} deployment queued.";
} else if ($type === 'App\Models\StandalonePostgresql') { } else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource); StartPostgresql::run($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);
$message->push("Database {$resource->name} started."); $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneRedis') { } else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource); StartRedis::run($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);
$message->push("Database {$resource->name} started."); $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMongodb') { } else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource); StartMongodb::run($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);
$message->push("Database {$resource->name} started."); $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMysql') { } else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource); StartMysql::run($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);
$message->push("Database {$resource->name} started."); $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMariadb') { } else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource); StartMariadb::run($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);
$message->push("Database {$resource->name} started."); $message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\Service') { } else if ($type === 'App\Models\Service') {
StartService::run($resource); StartService::run($resource);
$message->push("Service {$resource->name} started. It could take a while, be patient."); $message = "Service {$resource->name} started. It could take a while, be patient.";
} }
return $message; return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
} }
} }

View File

@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\InstanceSettings;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Domains extends Controller
{
public function domains(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = ModelsProject::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
$settings = InstanceSettings::get();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (!$settings->public_ipv4 && !$settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
$services = $projects->pluck('services')->flatten();
if ($services->count() > 0) {
foreach ($services as $service) {
$service_applications = $service->applications;
if ($service_applications->count() > 0) {
foreach ($service_applications as $application) {
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (!$settings->public_ipv4 && !$settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
}
}
$domains = $domains->groupBy('ip')->map(function ($domain) {
return $domain->pluck('domain')->flatten();
})->map(function ($domain, $ip) {
return [
'ip' => $ip,
'domains' => $domain,
];
})->values();
return response()->json($domains);
}
}

View File

@ -12,7 +12,7 @@ class Project extends Controller
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get(); $projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json($projects); return response()->json($projects);
@ -21,7 +21,7 @@ class Project extends Controller
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
return response()->json($project); return response()->json($project);
@ -30,7 +30,7 @@ class Project extends Controller
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
class Resources extends Controller
{
public function resources(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = Project::where('team_id', $teamId)->get();
$resources = collect();
$resources->push($projects->pluck('applications')->flatten());
$resources->push($projects->pluck('services')->flatten());
foreach (collect(DATABASE_TYPES) as $db) {
$resources->push($projects->pluck(str($db)->plural(2))->flatten());
}
$resources = $resources->flatten();
$resources = $resources->map(function ($resource) {
$payload = $resource->toArray();
if ($resource->getMorphClass() === 'App\Models\Service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
$payload['type'] = $resource->type();
return $payload;
});
return response()->json($resources);
}
}

View File

@ -12,7 +12,7 @@ class Server extends Controller
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable; $server['is_reachable'] = $server->settings->is_reachable;
@ -26,7 +26,7 @@ class Server extends Controller
$with_resources = $request->query('resources'); $with_resources = $request->query('resources');
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();
if (is_null($teamId)) { if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); return invalid_token();
} }
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) { if (is_null($server)) {

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class Team extends Controller
{
public function teams(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
return response()->json($teams);
}
public function team_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api/team-by-id"], 404);
}
return response()->json($team);
}
public function members_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api/team-by-id-members"], 404);
}
return response()->json($team->members);
}
public function current_team(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team);
}
public function current_team_members(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team->members);
}
}

View File

@ -21,7 +21,7 @@ class DecideWhatToDoWithUser
} }
if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect()->route('boarding'); return redirect()->route('onboarding');
} }
return $next($request); return $next($request);
} }
@ -43,7 +43,7 @@ class DecideWhatToDoWithUser
if (Str::startsWith($request->path(), 'invitations')) { if (Str::startsWith($request->path(), 'invitations')) {
return $next($request); return $next($request);
} }
return redirect()->route('boarding'); return redirect()->route('onboarding');
} }
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') { if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect(RouteServiceProvider::HOME); return redirect(RouteServiceProvider::HOME);

View File

@ -24,6 +24,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
use Spatie\Url\Url; use Spatie\Url\Url;
@ -92,6 +93,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $buildTarget = null; private ?string $buildTarget = null;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null; private ?string $full_healthcheck_url = null;
private bool $custom_healthcheck_found = false;
private string $serverUser = 'root'; private string $serverUser = 'root';
private string $serverUserHomeDir = '/root'; private string $serverUserHomeDir = '/root';
@ -239,6 +241,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(false); $this->application->isConfigurationChanged(false);
$this->run_post_deployment_command();
return; return;
} else if ($this->pull_request_id !== 0) { } else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
@ -273,6 +276,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED); ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
} }
} }
$this->run_post_deployment_command();
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
@ -294,13 +298,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"ignore_errors" => true, "ignore_errors" => true,
] ]
); );
$this->execute_remote_command( // $this->execute_remote_command(
[ // [
"docker image prune -f >/dev/null 2>&1", // "docker image prune -f >/dev/null 2>&1",
"hidden" => true, // "hidden" => true,
"ignore_errors" => true, // "ignore_errors" => true,
] // ]
); // );
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
} }
} }
@ -456,6 +460,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
$this->clone_repository();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
@ -467,7 +472,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return; return;
} }
} }
$this->clone_repository();
$this->cleanup_git(); $this->cleanup_git();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
@ -775,7 +779,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
// Implement healthcheck for swarm // Implement healthcheck for swarm
} else { } else {
if ($this->application->isHealthcheckDisabled()) { if ($this->application->isHealthcheckDisabled() && $this->custom_healthcheck_found === false) {
$this->newVersionIsHealthy = true; $this->newVersionIsHealthy = true;
return; return;
} }
@ -808,7 +812,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
break; break;
} }
$counter++; $counter++;
sleep($this->application->health_check_interval); Sleep::for($this->application->health_check_interval)->seconds();
} }
} }
} }
@ -873,8 +877,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[ [
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
], ],
); );
$this->run_pre_deployment_command();
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
{ {
@ -1077,7 +1081,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_compose_file() private function generate_compose_file()
{ {
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
$onlyPort = null;
if (count($ports) > 0) {
$onlyPort = $ports[0];
}
$persistent_storages = $this->generate_local_persistent_volumes(); $persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
@ -1088,6 +1095,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$labels = $labels->filter(function ($value, $key) { $labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.'); return !Str::startsWith($value, 'coolify.');
}); });
$found_caddy_labels = $labels->filter(function ($value, $key) {
return Str::startsWith($value, 'caddy_');
});
if ($found_caddy_labels->count() === 0) {
if ($this->pull_request_id !== 0) {
$domains = str(data_get($this->preview, 'fqdn'))->explode(',');
} else {
$domains = str(data_get($this->application, 'fqdn'))->explode(',');
}
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $this->application->destination->network,
uuid: $this->application->uuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $this->application->isForceHttpsEnabled(),
is_gzip_enabled: $this->application->isGzipEnabled(),
is_stripprefix_enabled: $this->application->isStripprefixEnabled()
));
}
$this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save(); $this->application->save();
} else { } else {
@ -1097,6 +1123,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
// Check for custom HEALTHCHECK
$this->custom_healthcheck_found = false;
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile', "ignore_errors" => true
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if (str($dockerfile)->contains('HEALTHCHECK')) {
$this->custom_healthcheck_found = true;
}
}
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@ -1109,16 +1147,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
], ],
'healthcheck' => [
'test' => [
'CMD-SHELL',
$this->generate_healthcheck_commands()
],
'interval' => $this->application->health_check_interval . 's',
'timeout' => $this->application->health_check_timeout . 's',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period . 's'
],
'mem_limit' => $this->application->limits_memory, 'mem_limit' => $this->application->limits_memory,
'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,
@ -1135,6 +1163,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if (!$this->custom_healthcheck_found) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
'CMD-SHELL',
$this->generate_healthcheck_commands()
],
'interval' => $this->application->health_check_interval . 's',
'timeout' => $this->application->health_check_timeout . 's',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period . 's'
];
}
if (!is_null($this->application->limits_cpuset)) { if (!is_null($this->application->limits_cpuset)) {
data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset); data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset);
} }
@ -1235,24 +1275,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
if ((bool)$this->application->settings->is_consistent_container_name_enabled) { if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->container_name], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
}
} else {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
data_forget($docker_compose, 'services.' . $this->container_name); data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
@ -1272,6 +1294,24 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
} }
} else {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->container_name], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
}
} }
} }
@ -1652,16 +1692,69 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]); ]);
} }
private function run_pre_deployment_command()
{
if (empty($this->application->pre_deployment_command)) {
return;
}
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($containers->count() == 0) {
return;
}
$this->application_deployment_queue->addLogEntry("Executing pre-deployment command (see debug log for output): {$this->application->pre_deployment_command}");
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container . '-' . $this->application->uuid)) {
$cmd = 'sh -c "' . str_replace('"', '\"', $this->application->pre_deployment_command) . '"';
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, $exec), 'hidden' => true
],
);
return;
}
}
throw new RuntimeException('Pre-deployment command: Could not find a valid container. Is the container name correct?');
}
private function run_post_deployment_command()
{
if (empty($this->application->post_deployment_command)) {
return;
}
$this->application_deployment_queue->addLogEntry("Executing post-deployment command (see debug log for output): {$this->application->post_deployment_command}");
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container . '-' . $this->application->uuid)) {
$cmd = 'sh -c "' . str_replace('"', '\"', $this->application->post_deployment_command) . '"';
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, $exec), 'hidden' => true
],
);
return;
}
}
throw new RuntimeException('Post-deployment command: Could not find a valid container. Is the container name correct?');
}
private function next(string $status) private function next(string $status)
{ {
queue_next_deployment($this->application); queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status // If the deployment is cancelled by the user, don't update the status
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value) { if (
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
) {
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => $status, 'status' => $status,
]); ]);
} }
if ($status === ApplicationDeploymentStatus::FAILED->value) { if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return; return;
} }

View File

@ -21,7 +21,8 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
public function __construct( public function __construct(
public Activity $activity, public Activity $activity,
public bool $ignore_errors = false, public bool $ignore_errors = false,
public $call_event_on_finish = null public $call_event_on_finish = null,
public $call_event_data = null
) { ) {
} }
@ -33,7 +34,8 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
$remote_process = resolve(RunRemoteProcess::class, [ $remote_process = resolve(RunRemoteProcess::class, [
'activity' => $this->activity, 'activity' => $this->activity,
'ignore_errors' => $this->ignore_errors, 'ignore_errors' => $this->ignore_errors,
'call_event_on_finish' => $this->call_event_on_finish 'call_event_on_finish' => $this->call_event_on_finish,
'call_event_data' => $this->call_event_data
]); ]);
$remote_process(); $remote_process();

View File

@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerFilesFromServerJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public ServiceApplication|ServiceDatabase $service)
{
}
public function handle()
{
$this->service->getFilesFromServer(isInit: true);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
use App\Models\LocalFileVolume;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerStorageSaveJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public LocalFileVolume $localFileVolume)
{
}
public function handle()
{
$this->localFileVolume->saveStorageOnServer();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Listeners;
use App\Events\ProxyStarted;
use App\Models\Server;
class ProxyStartedNotification
{
public Server $server;
public function __construct()
{
}
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDynamicProxyConfiguration();
}
}

View File

@ -4,7 +4,6 @@ namespace App\Livewire\Admin;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component; use Livewire\Component;
class Index extends Component class Index extends Component

View File

@ -73,7 +73,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function restartBoarding() public function restartBoarding()
{ {
return redirect()->route('boarding'); return redirect()->route('onboarding');
} }
public function skipBoarding() public function skipBoarding()
{ {
@ -126,6 +126,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
} }
public function getProxyType() public function getProxyType()
{ {
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK_V2->value); $this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
// $proxyTypeSet = $this->createdServer->proxy->type; // $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) { // if (!$proxyTypeSet) {

View File

@ -17,10 +17,14 @@ class LayoutPopups extends Component
{ {
$this->dispatch('success', 'Realtime events configured!'); $this->dispatch('success', 'Realtime events configured!');
} }
public function disable() public function disableSponsorship()
{ {
auth()->user()->update(['is_notification_sponsorship_enabled' => false]); auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
} }
public function disableNotifications()
{
auth()->user()->update(['is_notification_notifications_enabled' => false]);
}
public function render() public function render()
{ {
return view('livewire.layout-popups'); return view('livewire.layout-popups');

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Profile; namespace App\Livewire\Profile;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@ -10,6 +11,13 @@ class Index extends Component
public int $userId; public int $userId;
public string $email; public string $email;
#[Validate('required')]
public string $current_password;
#[Validate('required|min:8')]
public string $new_password;
#[Validate('required|min:8|same:new_password')]
public string $new_password_confirmation;
#[Validate('required')] #[Validate('required')]
public string $name; public string $name;
public function mount() public function mount()
@ -19,7 +27,6 @@ class Index extends Component
$this->email = auth()->user()->email; $this->email = auth()->user()->email;
} }
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->validate();
@ -27,7 +34,30 @@ class Index extends Component
'name' => $this->name, 'name' => $this->name,
]); ]);
$this->dispatch('success', 'Profile updated'); $this->dispatch('success', 'Profile updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function resetPassword()
{
try {
$this->validate();
if (!Hash::check($this->current_password, auth()->user()->password)) {
$this->dispatch('error', 'Current password is incorrect.');
return;
}
if ($this->new_password !== $this->new_password_confirmation) {
$this->dispatch('error', 'The two new passwords does not match.');
return;
}
auth()->user()->update([
'password' => Hash::make($this->new_password),
]);
$this->dispatch('success', 'Password updated.');
$this->current_password = '';
$this->new_password = '';
$this->new_password_confirmation = '';
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -66,6 +66,10 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable',
'application.custom_labels' => 'nullable', 'application.custom_labels' => 'nullable',
'application.custom_docker_run_options' => 'nullable', 'application.custom_docker_run_options' => 'nullable',
'application.pre_deployment_command' => 'nullable',
'application.pre_deployment_command_container' => 'nullable',
'application.post_deployment_command' => 'nullable',
'application.post_deployment_command_container' => 'nullable',
'application.settings.is_static' => 'boolean|required', 'application.settings.is_static' => 'boolean|required',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
@ -112,6 +116,10 @@ class General extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage()); $this->dispatch('error', $e->getMessage());
} }
if ($this->application->build_pack === 'dockercompose') {
$this->application->fqdn = null;
$this->application->settings->save();
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
@ -120,7 +128,7 @@ class General extends Component
} }
$this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
@ -163,7 +171,12 @@ class General extends Component
} }
return $domain; return $domain;
} }
public function updatedApplicationBaseDirectory() {
raY('asdf');
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
@ -211,12 +224,11 @@ class General extends Component
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save(); $this->application->save();
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
// $this->dispatch('success', 'Labels reset to default!');
} }
public function submit($showToaster = true) public function submit($showToaster = true)
{ {
try { try {
if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();

View File

@ -10,7 +10,8 @@ class Execution extends Component
public $backup; public $backup;
public $executions; public $executions;
public $s3s; public $s3s;
public function mount() { public function mount()
{
$backup_uuid = request()->route('backup_uuid'); $backup_uuid = request()->route('backup_uuid');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
@ -34,6 +35,11 @@ class Execution extends Component
$this->executions = $executions; $this->executions = $executions;
$this->s3s = currentTeam()->s3s; $this->s3s = currentTeam()->s3s;
} }
public function cleanupFailed()
{
$this->backup->executions()->where('status', 'failed')->delete();
$this->dispatch('refreshBackupExecutions');
}
public function render() public function render()
{ {
return view('livewire.project.database.backup.execution'); return view('livewire.project.database.backup.execution');

View File

@ -34,7 +34,7 @@ class BackupExecutions extends Component
} }
$execution->delete(); $execution->delete();
$this->dispatch('success', 'Backup deleted.'); $this->dispatch('success', 'Backup deleted.');
$this->dispatch('refreshBackupExecutions'); $this->refreshBackupExecutions();
} }
public function download($exeuctionId) public function download($exeuctionId)
{ {
@ -65,6 +65,6 @@ class BackupExecutions extends Component
} }
public function refreshBackupExecutions(): void public function refreshBackupExecutions(): void
{ {
$this->executions = data_get($this->backup, 'executions', []); $this->executions = $this->backup->executions()->get()->sortByDesc('created_at');
} }
} }

View File

@ -17,7 +17,6 @@ class DockerCompose extends Component
public array $query; public array $query;
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
if (isDev()) { if (isDev()) {
@ -40,12 +39,17 @@ class DockerCompose extends Component
} }
public function submit() public function submit()
{ {
$server_id = $this->query['server_id'];
try { try {
$this->validate([ $this->validate([
'dockerComposeRaw' => 'required' 'dockerComposeRaw' => 'required'
]); ]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$server_id = $this->query['server_id'];
$isValid = validateComposeFile($this->dockerComposeRaw, $server_id);
if ($isValid !== 'OK') {
return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
}
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
@ -74,7 +78,6 @@ class DockerCompose extends Component
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -38,8 +38,12 @@ class Configuration extends Component
} }
public function check_status() public function check_status()
{ {
dispatch_sync(new ContainerStatusJob($this->service->server)); try {
$this->dispatch('refresh')->self(); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->dispatch('serviceStatusChanged'); $this->dispatch('refresh')->self();
$this->dispatch('serviceStatusChanged');
} catch (\Exception $e) {
return handleError($e, $this);
}
} }
} }

View File

@ -41,7 +41,6 @@ class StackForm extends Component
} }
public function saveCompose($raw) public function saveCompose($raw)
{ {
$this->service->docker_compose_raw = $raw; $this->service->docker_compose_raw = $raw;
$this->submit(); $this->submit();
} }
@ -55,6 +54,10 @@ class StackForm extends Component
{ {
try { try {
$this->validate(); $this->validate();
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server->id);
if ($isValid !== 'OK') {
throw new \Exception("Invalid docker-compose file.\n$isValid");
}
$this->service->save(); $this->service->save();
$this->service->saveExtraFields($this->fields); $this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();

View File

@ -35,7 +35,7 @@ class ResourceLimits extends Component
if (!$this->resource->limits_memory_swap) { if (!$this->resource->limits_memory_swap) {
$this->resource->limits_memory_swap = "0"; $this->resource->limits_memory_swap = "0";
} }
if (!$this->resource->limits_memory_swappiness) { if (is_null($this->resource->limits_memory_swappiness)) {
$this->resource->limits_memory_swappiness = "60"; $this->resource->limits_memory_swappiness = "60";
} }
if (!$this->resource->limits_memory_reservation) { if (!$this->resource->limits_memory_reservation) {
@ -47,7 +47,7 @@ class ResourceLimits extends Component
if ($this->resource->limits_cpuset === "") { if ($this->resource->limits_cpuset === "") {
$this->resource->limits_cpuset = null; $this->resource->limits_cpuset = null;
} }
if (!$this->resource->limits_cpu_shares) { if (is_null($this->resource->limits_cpu_shares)) {
$this->resource->limits_cpu_shares = 1024; $this->resource->limits_cpu_shares = 1024;
} }
$this->validate(); $this->validate();

View File

@ -45,7 +45,7 @@ class ResourceOperations extends Component
'destination_id' => $new_destination->id, 'destination_id' => $new_destination->id,
]); ]);
$new_resource->save(); $new_resource->save();
if ($new_resource->destination->server->proxyType() === 'TRAEFIK_V2') { if ($new_resource->destination->server->proxyType() !== 'NONE') {
$customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n"); $customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n");
$new_resource->custom_labels = base64_encode($customLabels); $new_resource->custom_labels = base64_encode($customLabels);
$new_resource->save(); $new_resource->save();

View File

@ -89,6 +89,7 @@ class ByIp extends Component
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id, 'private_key_id' => $this->private_key_id,
'proxy' => [ 'proxy' => [
// set default proxy type to traefik v2
"type" => ProxyTypes::TRAEFIK_V2->value, "type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value, "status" => ProxyStatus::EXITED->value,
], ],

View File

@ -21,7 +21,7 @@ class Proxy extends Component
public function mount() public function mount()
{ {
$this->selectedProxy = data_get($this->server, 'proxy.type'); $this->selectedProxy = $this->server->proxyType();
$this->redirect_url = data_get($this->server, 'proxy.redirect_url'); $this->redirect_url = data_get($this->server, 'proxy.redirect_url');
} }
@ -54,8 +54,7 @@ class Proxy extends Component
SaveConfiguration::run($this->server, $this->proxy_settings); 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();
$this->server->setupDefault404Redirect();
setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server);
$this->dispatch('success', 'Proxy configuration saved.'); $this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@ -66,6 +65,9 @@ class Proxy extends Component
{ {
try { try {
$this->proxy_settings = CheckConfiguration::run($this->server, true); $this->proxy_settings = CheckConfiguration::run($this->server, true);
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->save();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -14,9 +14,17 @@ class DynamicConfigurationNavbar extends Component
public function delete(string $fileName) public function delete(string $fileName)
{ {
$server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); $server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first();
$proxy_path = get_proxy_path(); $proxy_path = $server->proxyPath();
$proxy_type = $server->proxyType();
$file = str_replace('|', '.', $fileName); $file = str_replace('|', '.', $fileName);
if ($proxy_type === 'CADDY' && $file === "Caddyfile") {
$this->dispatch('error', 'Cannot delete Caddyfile.');
return;
}
instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server); instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server);
if ($proxy_type === 'CADDY') {
$server->reloadCaddy();
}
$this->dispatch('success', 'File deleted.'); $this->dispatch('success', 'File deleted.');
$this->dispatch('loadDynamicConfigurations'); $this->dispatch('loadDynamicConfigurations');
$this->dispatch('refresh'); $this->dispatch('refresh');

View File

@ -11,26 +11,32 @@ class DynamicConfigurations extends Component
public ?Server $server = null; public ?Server $server = null;
public $parameters = []; public $parameters = [];
public Collection $contents; public Collection $contents;
protected $listeners = ['loadDynamicConfigurations', 'refresh' => '$refresh']; public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
'loadDynamicConfigurations',
'refresh' => '$refresh'
];
}
protected $rules = [ protected $rules = [
'contents.*' => 'nullable|string', 'contents.*' => 'nullable|string',
]; ];
public function loadDynamicConfigurations() public function loadDynamicConfigurations()
{ {
$proxy_path = get_proxy_path(); $proxy_path = $this->server->proxyPath();
$files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server); $files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server);
$files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file)); $files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file));
$files = $files->map(fn ($file) => trim($file)); $files = $files->map(fn ($file) => trim($file));
$files = $files->sort(); $files = $files->sort();
if ($files->contains('coolify.yaml')) {
$files = $files->filter(fn ($file) => $file !== 'coolify.yaml')->prepend('coolify.yaml');
}
$contents = collect([]); $contents = collect([]);
foreach ($files as $file) { foreach ($files as $file) {
$without_extension = str_replace('.', '|', $file); $without_extension = str_replace('.', '|', $file);
$contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
} }
$this->contents = $contents; $this->contents = $contents;
$this->dispatch('refresh');
} }
public function mount() public function mount()
{ {

View File

@ -29,7 +29,6 @@ class NewDynamicConfiguration extends Component
'fileName' => 'required', 'fileName' => 'required',
'value' => 'required', 'value' => 'required',
]); ]);
if (data_get($this->parameters, 'server_uuid')) { if (data_get($this->parameters, 'server_uuid')) {
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
} }
@ -39,14 +38,21 @@ class NewDynamicConfiguration extends Component
if (is_null($this->server)) { if (is_null($this->server)) {
return redirect()->route('server.index'); return redirect()->route('server.index');
} }
if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) { $proxy_type = $this->server->proxyType();
$this->fileName = "{$this->fileName}.yaml"; if ($proxy_type === 'TRAEFIK_V2') {
if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) {
$this->fileName = "{$this->fileName}.yaml";
}
if ($this->fileName === 'coolify.yaml') {
$this->dispatch('error', 'File name is reserved.');
return;
}
} else if ($proxy_type === 'CADDY') {
if (!str($this->fileName)->endsWith('.caddy')) {
$this->fileName = "{$this->fileName}.caddy";
}
} }
if ($this->fileName === 'coolify.yaml') { $proxy_path = $this->server->proxyPath();
$this->dispatch('error', 'File name is reserved.');
return;
}
$proxy_path = get_proxy_path();
$file = "{$proxy_path}/dynamic/{$this->fileName}"; $file = "{$proxy_path}/dynamic/{$this->fileName}";
if ($this->newFile) { if ($this->newFile) {
$exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server); $exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server);
@ -55,11 +61,18 @@ class NewDynamicConfiguration extends Component
return; return;
} }
} }
$yaml = Yaml::parse($this->value); if ($proxy_type === 'TRAEFIK_V2') {
$yaml = Yaml::dump($yaml, 10, 2); $yaml = Yaml::parse($this->value);
$this->value = $yaml; $yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml;
}
$base64_value = base64_encode($this->value); $base64_value = base64_encode($this->value);
instant_remote_process(["echo '{$base64_value}' | base64 -d > {$file}"], $this->server); instant_remote_process([
"echo '{$base64_value}' | base64 -d > {$file}",
], $this->server);
if ($proxy_type === 'CADDY') {
$this->server->reloadCaddy();
}
$this->dispatch('loadDynamicConfigurations'); $this->dispatch('loadDynamicConfigurations');
$this->dispatch('dynamic-configuration-added'); $this->dispatch('dynamic-configuration-added');
$this->dispatch('success', 'Dynamic configuration saved.'); $this->dispatch('success', 'Dynamic configuration saved.');

View File

@ -2,12 +2,9 @@
namespace App\Livewire\Settings; namespace App\Livewire\Settings;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings; use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class Configuration extends Component class Configuration extends Component
{ {
@ -78,13 +75,7 @@ class Configuration extends Component
$this->settings->save(); $this->settings->save();
$this->server = Server::findOrFail(0); $this->server = Server::findOrFail(0);
$this->setup_instance_fqdn(); $this->server->setupDynamicProxyConfiguration();
$this->dispatch('success', 'Instance settings updated successfully!'); $this->dispatch('success', 'Instance settings updated successfully!');
} }
private function setup_instance_fqdn()
{
setup_dynamic_configuration();
}
} }

View File

@ -24,6 +24,8 @@ class Change extends Component
public string $name; public string $name;
public bool $is_system_wide; public bool $is_system_wide;
public $applications;
protected $rules = [ protected $rules = [
'github_app.name' => 'required|string', 'github_app.name' => 'required|string',
'github_app.organization' => 'nullable|string', 'github_app.organization' => 'nullable|string',
@ -90,6 +92,7 @@ class Change extends Component
if (!$this->github_app) { if (!$this->github_app) {
return redirect()->route('source.all'); return redirect()->route('source.all');
} }
$this->applications = $this->github_app->applications;
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
@ -170,6 +173,11 @@ class Change extends Component
public function delete() public function delete()
{ {
try { try {
if ($this->github_app->applications->isNotEmpty()) {
$this->dispatch('error', 'This source is being used by an application. Please delete all applications first.');
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
return;
}
$this->github_app->delete(); $this->github_app->delete();
return redirect()->route('source.all'); return redirect()->route('source.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@ -13,7 +13,7 @@ class LocalFileVolume extends BaseModel
{ {
static::created(function (LocalFileVolume $fileVolume) { static::created(function (LocalFileVolume $fileVolume) {
$fileVolume->load(['service']); $fileVolume->load(['service']);
$fileVolume->saveStorageOnServer(); dispatch(new \App\Jobs\ServerStorageSaveJob($fileVolume));
}); });
} }
public function service() public function service()

View File

@ -27,7 +27,8 @@ class Project extends BaseModel
$project->settings()->delete(); $project->settings()->delete();
}); });
} }
public function environment_variables() { public function environment_variables()
{
return $this->hasMany(SharedEnvironmentVariable::class); return $this->hasMany(SharedEnvironmentVariable::class);
} }
public function environments() public function environments()
@ -45,6 +46,10 @@ class Project extends BaseModel
return $this->belongsTo(Team::class); return $this->belongsTo(Team::class);
} }
public function services()
{
return $this->hasManyThrough(Service::class, Environment::class);
}
public function applications() public function applications()
{ {
return $this->hasManyThrough(Application::class, Environment::class); return $this->hasManyThrough(Application::class, Environment::class);
@ -70,7 +75,8 @@ class Project extends BaseModel
{ {
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
} }
public function resource_count() { public function resource_count()
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count(); return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count();
} }
} }

View File

@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived; use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
@ -15,6 +14,8 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel class Server extends BaseModel
{ {
@ -118,18 +119,304 @@ class Server extends BaseModel
} }
} }
} }
public function setupDefault404Redirect()
{
$dynamic_conf_path = $this->proxyPath() . "/dynamic";
$proxy_type = $this->proxyType();
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === 'TRAEFIK_V2') {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} else if ($proxy_type === 'CADDY') {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if ($proxy_type === 'CADDY') {
$conf = ":80, :443 {
respond 404
}";
$conf =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d > $default_redirect_file",
], $this);
$this->reloadCaddy();
return;
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $default_redirect_file",
], $this);
return;
}
if ($proxy_type === 'TRAEFIK_V2') {
$dynamic_conf = [
'http' =>
[
'routers' =>
[
'catchall' =>
[
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => "HostRegexp(`{catchall:.*}`)",
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp@file',
],
],
],
'services' =>
[
'noop' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => '',
],
],
],
],
],
'middlewares' =>
[
'redirect-regexp' =>
[
'redirectRegex' =>
[
'regex' => '(.*)',
'replacement' => $redirect_url,
'permanent' => false,
],
],
],
],
];
$conf = Yaml::dump($dynamic_conf, 12, 2);
$conf =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
} else if ($proxy_type === 'CADDY') {
$conf = ":80, :443 {
redir $redirect_url
}";
$conf =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d > $default_redirect_file",
], $this);
if (config('app.env') == 'local') {
ray($conf);
}
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
}
public function setupDynamicProxyConfiguration()
{
$settings = InstanceSettings::get();
$dynamic_config_path = $this->proxyPath() . "/dynamic";
if ($this->proxyType() === 'TRAEFIK_V2') {
$file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn)) {
instant_remote_process([
"rm -f $file",
], $this);
} else {
$url = Url::fromString($settings->fqdn);
$host = $url->getHost();
$schema = $url->getScheme();
$traefik_dynamic_conf = [
'http' =>
[
'middlewares' => [
'redirect-to-https' => [
'redirectscheme' => [
'scheme' => 'https',
],
],
'gzip' => [
'compress' => true,
],
],
'routers' =>
[
'coolify-http' =>
[
'middlewares' => [
0 => 'gzip',
],
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify',
'rule' => "Host(`{$host}`)",
],
'coolify-realtime-ws' =>
[
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
],
],
'services' =>
[
'coolify' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => 'http://coolify:80',
],
],
],
],
'coolify-realtime' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => 'http://coolify-realtime:6001',
],
],
],
],
],
],
];
if ($schema === 'https') {
$traefik_dynamic_conf['http']['routers']['coolify-http']['middlewares'] = [
0 => 'redirect-to-https',
];
$traefik_dynamic_conf['http']['routers']['coolify-https'] = [
'entryPoints' => [
0 => 'https',
],
'service' => 'coolify',
'rule' => "Host(`{$host}`)",
'tls' => [
'certresolver' => 'letsencrypt',
],
];
$traefik_dynamic_conf['http']['routers']['coolify-realtime-wss'] = [
'entryPoints' => [
0 => 'https',
],
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
'tls' => [
'certresolver' => 'letsencrypt',
],
];
}
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$yaml;
$base64 = base64_encode($yaml);
instant_remote_process([
"mkdir -p $dynamic_config_path",
"echo '$base64' | base64 -d > $file",
], $this);
if (config('app.env') == 'local') {
// ray($yaml);
}
}
} else if ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
if (empty($settings->fqdn)) {
instant_remote_process([
"rm -f $file",
], $this);
$this->reloadCaddy();
} else {
$url = Url::fromString($settings->fqdn);
$host = $url->getHost();
$schema = $url->getScheme();
$caddy_file = "
$schema://$host {
handle /app/* {
reverse_proxy coolify-realtime:6001
}
reverse_proxy coolify:80
}";
$base64 = base64_encode($caddy_file);
instant_remote_process([
"echo '$base64' | base64 -d > $file",
], $this);
$this->reloadCaddy();
}
}
}
public function reloadCaddy()
{
return instant_remote_process([
"docker exec coolify-proxy caddy reload --config /config/caddy/Caddyfile.autosave",
], $this);
}
public function proxyPath()
{
$base_path = config('coolify.base_config_path');
$proxyType = $this->proxyType();
$proxy_path = "$base_path/proxy";
// TODO: should use /traefik for already exisiting configurations?
// Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK_V2->value) {
$proxy_path = $proxy_path;
} else if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = $proxy_path . '/caddy';
} else if ($proxyType === ProxyTypes::NGINX->value) {
$proxy_path = $proxy_path . '/nginx';
}
return $proxy_path;
}
public function proxyType() public function proxyType()
{ {
$proxyType = $this->proxy->get('type'); // $proxyType = $this->proxy->get('type');
if ($proxyType === ProxyTypes::NONE->value) { // if ($proxyType === ProxyTypes::NONE->value) {
return $proxyType; // return $proxyType;
} // }
if (is_null($proxyType)) { // if (is_null($proxyType)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value; // $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value; // $this->proxy->status = ProxyStatus::EXITED->value;
$this->save(); // $this->save();
} // }
return $this->proxy->get('type'); return data_get($this->proxy, 'type');
} }
public function scopeWithProxy(): Builder public function scopeWithProxy(): Builder
{ {

View File

@ -102,6 +102,29 @@ class Service extends BaseModel
foreach ($applications as $application) { foreach ($applications as $application) {
$image = str($application->image)->before(':')->value(); $image = str($application->image)->before(':')->value();
switch ($image) { switch ($image) {
case str($image)?->contains('grafana'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
$data = $data->merge([
'Admin User' => [
'key' => 'GF_SECURITY_ADMIN_USER',
'value' => 'admin',
'readonly' => true,
'rules' => 'required',
],
]);
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'GF_SECURITY_ADMIN_PASSWORD',
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Grafana', $data);
break;
case str($image)?->contains('directus'): case str($image)?->contains('directus'):
$data = collect([]); $data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();

View File

@ -48,13 +48,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
} }
return explode(',', $recipients); return explode(',', $recipients);
} }
static public function serverLimitReached() { static public function serverLimitReached()
{
$serverLimit = Team::serverLimit(); $serverLimit = Team::serverLimit();
$team = currentTeam(); $team = currentTeam();
$servers = $team->servers->count(); $servers = $team->servers->count();
return $servers >= $serverLimit; return $servers >= $serverLimit;
} }
public function serverOverflow() { public function serverOverflow()
{
if ($this->serverLimit() < $this->servers->count()) { if ($this->serverLimit() < $this->servers->count()) {
return true; return true;
} }
@ -170,4 +172,17 @@ class Team extends Model implements SendsDiscord, SendsEmail
]); ]);
} }
} }
public function isAnyNotificationEnabled()
{
if (isCloud()) {
return true;
}
if (!data_get(auth()->user(), 'is_notification_notifications_enabled')) {
return true;
}
if ($this->smtp_enabled || $this->resend_enabled || $this->discord_enabled || $this->telegram_enabled || $this->use_instance_email_settings) {
return true;
}
return false;
}
} }

View File

@ -26,6 +26,8 @@ class User extends Authenticatable implements SendsEmail
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
]; ];
protected $casts = [ protected $casts = [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',

View File

@ -2,8 +2,10 @@
namespace App\Providers; namespace App\Providers;
use App\Events\ProxyStarted;
use App\Listeners\MaintenanceModeDisabledNotification; use App\Listeners\MaintenanceModeDisabledNotification;
use App\Listeners\MaintenanceModeEnabledNotification; use App\Listeners\MaintenanceModeEnabledNotification;
use App\Listeners\ProxyStartedNotification;
use Illuminate\Foundation\Events\MaintenanceModeDisabled; use Illuminate\Foundation\Events\MaintenanceModeDisabled;
use Illuminate\Foundation\Events\MaintenanceModeEnabled; use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -17,9 +19,9 @@ class EventServiceProvider extends ServiceProvider
MaintenanceModeDisabled::class => [ MaintenanceModeDisabled::class => [
MaintenanceModeDisabledNotification::class, MaintenanceModeDisabledNotification::class,
], ],
// Registered::class => [ ProxyStarted::class => [
// SendEmailVerificationNotification::class, ProxyStartedNotification::class,
// ], ],
]; ];
public function boot(): void public function boot(): void
{ {

View File

@ -5,3 +5,7 @@ function get_team_id_from_token()
$token = auth()->user()->currentAccessToken(); $token = auth()->user()->currentAccessToken();
return data_get($token, 'team_id'); return data_get($token, 'team_id');
} }
function invalid_token()
{
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Enums\ProxyTypes;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
@ -215,6 +216,45 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
} }
return $payload; return $payload;
} }
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null)
{
$labels = collect([]);
if ($serviceLabels) {
$labels->push("caddy_ingress_network={$uuid}");
} else {
$labels->push("caddy_ingress_network={$network}");
}
foreach ($domains as $loop => $domain) {
$loop = $loop;
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
// $stripped_path = str($path)->replaceEnd('/', '');
$schema = $url->getScheme();
$port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
}
$labels->push("caddy_{$loop}={$schema}://{$host}");
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
if ($port) {
$labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}");
} else {
$labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}");
}
$labels->push("caddy_{$loop}.handle_path={$path}*");
if ($is_gzip_enabled) {
$labels->push("caddy_{$loop}.encode=zstd gzip");
}
if (isDev()) {
// $labels->push("caddy_{$loop}.tls=internal");
}
}
return $labels->sort();
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null)
{ {
$labels = collect([]); $labels = collect([]);
@ -395,7 +435,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} else { } else {
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels
$labels = $labels->merge(fqdnLabelsForTraefik( $labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid, uuid: $appUuid,
domains: $domains, domains: $domains,
@ -404,6 +444,16 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_gzip_enabled: $application->isGzipEnabled(), is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled() is_stripprefix_enabled: $application->isStripprefixEnabled()
)); ));
// Add Caddy labels
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
} }
return $labels->all(); return $labels->all();
} }
@ -506,3 +556,25 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
} }
return $compose_options->toArray(); return $compose_options->toArray();
} }
function validateComposeFile(string $compose, int $server_id): string|Throwable {
return 'OK';
try {
$uuid = Str::random(10);
$server = Server::findOrFail($server_id);
$base64_compose = base64_encode($compose);
$output = instant_remote_process([
"echo {$base64_compose} | base64 -d > /tmp/{$uuid}.yml",
"docker compose -f /tmp/{$uuid}.yml config",
], $server);
ray($output);
return 'OK';
} catch (\Throwable $e) {
ray($e);
return $e->getMessage();
} finally {
instant_remote_process([
"rm /tmp/{$uuid}.yml",
], $server);
}
}

View File

@ -7,12 +7,7 @@ use App\Models\Server;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
function get_proxy_path()
{
$base_path = config('coolify.base_config_path');
$proxy_path = "$base_path/proxy";
return $proxy_path;
}
function connectProxyToNetworks(Server $server) function connectProxyToNetworks(Server $server)
{ {
if ($server->isSwarm()) { if ($server->isSwarm()) {
@ -75,7 +70,9 @@ function connectProxyToNetworks(Server $server)
} }
function generate_default_proxy_configuration(Server $server) function generate_default_proxy_configuration(Server $server)
{ {
$proxy_path = get_proxy_path(); $proxy_path = $server->proxyPath();
$proxy_type = $server->proxyType();
if ($server->isSwarm()) { if ($server->isSwarm()) {
$networks = collect($server->swarmDockers)->map(function ($docker) { $networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
@ -98,287 +95,126 @@ function generate_default_proxy_configuration(Server $server)
"external" => true, "external" => true,
]; ];
}); });
$labels = [ if ($proxy_type === 'TRAEFIK_V2') {
"traefik.enable=true", $labels = [
"traefik.http.routers.traefik.entrypoints=http", "traefik.enable=true",
"traefik.http.routers.traefik.service=api@internal", "traefik.http.routers.traefik.entrypoints=http",
"traefik.http.services.traefik.loadbalancer.server.port=8080", "traefik.http.routers.traefik.service=api@internal",
"coolify.managed=true", "traefik.http.services.traefik.loadbalancer.server.port=8080",
]; "coolify.managed=true",
$config = [ ];
"version" => "3.8", $config = [
"networks" => $array_of_networks->toArray(), "version" => "3.8",
"services" => [ "networks" => $array_of_networks->toArray(),
"traefik" => [ "services" => [
"container_name" => "coolify-proxy", "traefik" => [
"image" => "traefik:v2.10", "container_name" => "coolify-proxy",
"restart" => RESTART_MODE, "image" => "traefik:v2.10",
"extra_hosts" => [ "restart" => RESTART_MODE,
"host.docker.internal:host-gateway", "extra_hosts" => [
"host.docker.internal:host-gateway",
],
"networks" => $networks->toArray(),
"ports" => [
"80:80",
"443:443",
"8080:8080",
],
"healthcheck" => [
"test" => "wget -qO- http://localhost:80/ping || exit 1",
"interval" => "4s",
"timeout" => "2s",
"retries" => 5,
],
"volumes" => [
"/var/run/docker.sock:/var/run/docker.sock:ro",
"{$proxy_path}:/traefik",
],
"command" => [
"--ping=true",
"--ping.entrypoint=http",
"--api.dashboard=true",
"--api.insecure=false",
"--entrypoints.http.address=:80",
"--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true",
"--entryPoints.http.http2.maxConcurrentStreams=50",
"--entrypoints.https.http.encodequerysemicolons=true",
"--entryPoints.https.http2.maxConcurrentStreams=50",
"--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true",
"--certificatesresolvers.letsencrypt.acme.httpchallenge=true",
"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json",
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
],
"labels" => $labels,
], ],
"networks" => $networks->toArray(),
"ports" => [
"80:80",
"443:443",
"8080:8080",
],
"healthcheck" => [
"test" => "wget -qO- http://localhost:80/ping || exit 1",
"interval" => "4s",
"timeout" => "2s",
"retries" => 5,
],
"volumes" => [
"/var/run/docker.sock:/var/run/docker.sock:ro",
"{$proxy_path}:/traefik",
],
"command" => [
"--ping=true",
"--ping.entrypoint=http",
"--api.dashboard=true",
"--api.insecure=false",
"--entrypoints.http.address=:80",
"--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true",
"--entryPoints.http.http2.maxConcurrentStreams=50",
"--entrypoints.https.http.encodequerysemicolons=true",
"--entryPoints.https.http2.maxConcurrentStreams=50",
"--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true",
"--certificatesresolvers.letsencrypt.acme.httpchallenge=true",
"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json",
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
],
"labels" => $labels,
], ],
], ];
]; if (isDev()) {
if (isDev()) { // $config['services']['traefik']['command'][] = "--log.level=debug";
// $config['services']['traefik']['command'][] = "--log.level=debug"; $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; }
} if ($server->isSwarm()) {
if ($server->isSwarm()) { data_forget($config, 'services.traefik.container_name');
data_forget($config, 'services.traefik.container_name'); data_forget($config, 'services.traefik.restart');
data_forget($config, 'services.traefik.restart'); data_forget($config, 'services.traefik.labels');
data_forget($config, 'services.traefik.labels');
$config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true";
$config['services']['traefik']['deploy'] = [ $config['services']['traefik']['deploy'] = [
"labels" => $labels, "labels" => $labels,
"placement" => [ "placement" => [
"constraints" => [ "constraints" => [
"node.role==manager", "node.role==manager",
],
],
];
} else {
$config['services']['traefik']['command'][] = "--providers.docker=true";
}
} else if ($proxy_type === 'CADDY') {
$config = [
"version" => "3.8",
"networks" => $array_of_networks->toArray(),
"services" => [
"caddy" => [
"container_name" => "coolify-proxy",
"image" => "lucaslorentz/caddy-docker-proxy:2.8-alpine",
"restart" => RESTART_MODE,
"extra_hosts" => [
"host.docker.internal:host-gateway",
],
"environment" => [
"CADDY_DOCKER_POLLING_INTERVAL=5s",
"CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile",
],
"networks" => $networks->toArray(),
"ports" => [
"80:80",
"443:443",
],
// "healthcheck" => [
// "test" => "wget -qO- http://localhost:80|| exit 1",
// "interval" => "4s",
// "timeout" => "2s",
// "retries" => 5,
// ],
"volumes" => [
"/var/run/docker.sock:/var/run/docker.sock:ro",
"{$proxy_path}/dynamic:/dynamic",
"{$proxy_path}/config:/config",
"{$proxy_path}/data:/data",
],
], ],
], ],
]; ];
} else { } else {
$config['services']['traefik']['command'][] = "--providers.docker=true"; return null;
} }
$config = Yaml::dump($config, 12, 2); $config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $config); SaveConfiguration::run($server, $config);
return $config; return $config;
} }
function setup_dynamic_configuration()
{
$dynamic_config_path = get_proxy_path() . "/dynamic";
$settings = InstanceSettings::get();
$server = Server::find(0);
if ($server) {
$file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn)) {
instant_remote_process([
"rm -f $file",
], $server);
} else {
$url = Url::fromString($settings->fqdn);
$host = $url->getHost();
$schema = $url->getScheme();
$traefik_dynamic_conf = [
'http' =>
[
'middlewares' => [
'redirect-to-https' => [
'redirectscheme' => [
'scheme' => 'https',
],
],
'gzip' => [
'compress' => true,
],
],
'routers' =>
[
'coolify-http' =>
[
'middlewares' => [
0 => 'gzip',
],
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify',
'rule' => "Host(`{$host}`)",
],
'coolify-realtime-ws' =>
[
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
],
],
'services' =>
[
'coolify' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => 'http://coolify:80',
],
],
],
],
'coolify-realtime' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => 'http://coolify-realtime:6001',
],
],
],
],
],
],
];
if ($schema === 'https') {
$traefik_dynamic_conf['http']['routers']['coolify-http']['middlewares'] = [
0 => 'redirect-to-https',
];
$traefik_dynamic_conf['http']['routers']['coolify-https'] = [
'entryPoints' => [
0 => 'https',
],
'service' => 'coolify',
'rule' => "Host(`{$host}`)",
'tls' => [
'certresolver' => 'letsencrypt',
],
];
$traefik_dynamic_conf['http']['routers']['coolify-realtime-wss'] = [
'entryPoints' => [
0 => 'https',
],
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
'tls' => [
'certresolver' => 'letsencrypt',
],
];
}
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$yaml;
$base64 = base64_encode($yaml);
instant_remote_process([
"mkdir -p $dynamic_config_path",
"echo '$base64' | base64 -d > $file",
], $server);
if (config('app.env') == 'local') {
// ray($yaml);
}
}
}
}
function setup_default_redirect_404(string|null $redirect_url, Server $server)
{
$traefik_dynamic_conf_path = get_proxy_path() . "/dynamic";
$traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml";
if (empty($redirect_url)) {
instant_remote_process([
"mkdir -p $traefik_dynamic_conf_path",
"rm -f $traefik_default_redirect_file",
], $server);
} else {
$traefik_dynamic_conf = [
'http' =>
[
'routers' =>
[
'catchall' =>
[
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => "HostRegexp(`{catchall:.*}`)",
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp@file',
],
],
],
'services' =>
[
'noop' =>
[
'loadBalancer' =>
[
'servers' =>
[
0 =>
[
'url' => '',
],
],
],
],
],
'middlewares' =>
[
'redirect-regexp' =>
[
'redirectRegex' =>
[
'regex' => '(.*)',
'replacement' => $redirect_url,
'permanent' => false,
],
],
],
],
];
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml =
"# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n" .
$yaml;
$base64 = base64_encode($yaml);
instant_remote_process([
"mkdir -p $traefik_dynamic_conf_path",
"echo '$base64' | base64 -d > $traefik_default_redirect_file",
], $server);
if (config('app.env') == 'local') {
ray($yaml);
}
}
}

View File

@ -24,7 +24,8 @@ function remote_process(
?string $type_uuid = null, ?string $type_uuid = null,
?Model $model = null, ?Model $model = null,
bool $ignore_errors = false, bool $ignore_errors = false,
$callEventOnFinish = null $callEventOnFinish = null,
$callEventData = null
): Activity { ): Activity {
if (is_null($type)) { if (is_null($type)) {
$type = ActivityTypes::INLINE->value; $type = ActivityTypes::INLINE->value;
@ -50,6 +51,7 @@ function remote_process(
model: $model, model: $model,
ignore_errors: $ignore_errors, ignore_errors: $ignore_errors,
call_event_on_finish: $callEventOnFinish, call_event_on_finish: $callEventOnFinish,
call_event_data: $callEventData,
), ),
])(); ])();
} }

View File

@ -80,7 +80,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
return handleError($e); return handleError($e);
} }
} }
function updateCompose($resource) function updateCompose(ServiceApplication|ServiceDatabase $resource)
{ {
try { try {
$name = data_get($resource, 'name'); $name = data_get($resource, 'name');
@ -90,6 +90,9 @@ function updateCompose($resource)
// Switch Image // Switch Image
$image = data_get($resource, 'image'); $image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image); data_set($dockerCompose, "services.{$name}.image", $image);
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
if (!str($resource->fqdn)->contains(',')) { if (!str($resource->fqdn)->contains(',')) {
// Update FQDN // Update FQDN
@ -105,7 +108,6 @@ function updateCompose($resource)
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($resource->fqdn); $url = Url::fromString($resource->fqdn);
$url = $url->getHost(); $url = $url->getHost();
ray($url);
if ($generatedEnv) { if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://'); $url = Str::of($resource->fqdn)->after('://');
$generatedEnv->value = $url; $generatedEnv->value = $url;
@ -113,9 +115,6 @@ function updateCompose($resource)
} }
} }
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e);
} }

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Jobs\ServerFilesFromServerJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
@ -615,7 +616,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} }
$allServices = getServiceTemplates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
@ -630,7 +631,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} }
$definedNetwork = collect([$resource->uuid]); $definedNetwork = collect([$resource->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) { $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices) {
// Workarounds for beta users.
if ($serviceName === 'registry') {
$tempServiceName = "docker-registry";
} else {
$tempServiceName = $serviceName;
}
if (str(data_get($service, 'image'))->contains('glitchtip')) {
$tempServiceName = 'glitchtip';
}
$serviceDefinition = data_get($allServices, $tempServiceName);
$predefinedPort = data_get($serviceDefinition, 'port');
if ($serviceName === 'plausible') {
$predefinedPort = '8000';
}
// End of workarounds for beta users.
$serviceVolumes = collect(data_get($service, 'volumes', [])); $serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', [])); $servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', [])); $serviceNetworks = collect(data_get($service, 'networks', []));
@ -852,7 +868,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
] ]
); );
} }
$savedService->getFilesFromServer(isInit: true); dispatch(new ServerFilesFromServerJob($savedService));
return $volume; return $volume;
}); });
data_set($service, 'volumes', $serviceVolumes->toArray()); data_set($service, 'volumes', $serviceVolumes->toArray());
@ -898,17 +914,24 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// SERVICE_FQDN_UMAMI_1000 // SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_'); $port = $key->afterLast('_');
} else { } else {
// SERVICE_FQDN_UMAMI $last = $key->afterLast('_');
$port = null; if (is_numeric($last->value())) {
// SERVICE_FQDN_3001
$port = $last;
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
} }
if ($port) { if ($port) {
$fqdn = "$fqdn:$port"; $fqdn = "$fqdn:$port";
} }
if (substr_count($key->value(), '_') >= 2) { if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) { if ($value) {
$value = Str::of('/'); $path = $value->value();
} else {
$path = null;
} }
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) { if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value()); $alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) { if ($alreadyGenerated) {
@ -939,6 +962,25 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'is_preview' => false, 'is_preview' => false,
]); ]);
} }
// Caddy needs exact port in some cases.
if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) {
if ($resource->server->proxyType() === 'CADDY') {
$env = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
])->first();
if ($env) {
$env_url = Url::fromString($savedService->fqdn);
$env_port = $env_url->getPort();
if ($env_port !== $predefinedPort) {
$env_url = $env_url->withPort($predefinedPort);
$savedService->fqdn = $env_url->__toString();
$savedService->save();
}
}
}
}
// data_forget($service, "environment.$variableName"); // data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName"); // $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) { // if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
@ -987,6 +1029,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
// Caddy needs exact port in some cases.
if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') {
$env = EnvironmentVariable::where([
'key' => $key,
'service_id' => $resource->id,
])->first();
if ($env) {
$env_url = Url::fromString($env->value);
$env_port = $env_url->getPort();
if ($env_port !== $predefinedPort) {
$env_url = $env_url->withPort($predefinedPort);
$savedService->fqdn = $env_url->__toString();
$savedService->save();
}
}
}
} }
} else { } else {
$generatedValue = generateEnvValue($command, $resource); $generatedValue = generateEnvValue($command, $resource);
@ -1056,6 +1114,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(), is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName service_name: $serviceName
)); ));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
network: $resource->destination->network,
uuid: $resource->uuid,
domains: $fqdns,
is_force_https_enabled: true,
serviceLabels: $serviceLabels,
is_gzip_enabled: $savedService->isGzipEnabled(),
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName
));
} }
} }
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@ -1354,10 +1422,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdn = "$fqdn:$port"; $fqdn = "$fqdn:$port";
} }
if (substr_count($key->value(), '_') >= 2) { if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) { if ($value) {
$value = Str::of('/'); $path = $value->value();
} else {
$path = null;
} }
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) { if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value()); $alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) { if ($alreadyGenerated) {
@ -1495,7 +1564,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $preview_fqdn; return $preview_fqdn;
}); });
} }
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns, serviceLabels: $serviceLabels)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $uuid,
domains: $fqdns,
serviceLabels: $serviceLabels
));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
network: $resource->destination->network,
uuid: $uuid,
domains: $fqdns,
serviceLabels: $serviceLabels
));
} }
} }
} }

View File

@ -135,7 +135,7 @@ function allowedPathsForBoardingAccounts()
{ {
return [ return [
...allowedPathsForUnsubscribedAccounts(), ...allowedPathsForUnsubscribedAccounts(),
'boarding', 'onboarding',
'livewire/update' 'livewire/update'
]; ];
} }

View File

@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://1bbc8f762199a52aee39196adb3e8d1a@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://f0b0e6be13926d4ac68d68d51d38db8f@o1082494.ingest.us.sentry.io/4505347448045568',
// 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.236', 'release' => '4.0.0-beta.238',
// 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.236'; return '4.0.0-beta.238';

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::table('applications', function (Blueprint $table) {
$table->string('post_deployment_command')->nullable();
$table->string('post_deployment_command_container')->nullable();
$table->string('pre_deployment_command')->nullable();
$table->string('pre_deployment_command_container')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('post_deployment_command');
$table->dropColumn('post_deployment_command_container');
$table->dropColumn('pre_deployment_command');
$table->dropColumn('pre_deployment_command_container');
});
}
};

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('users', function (Blueprint $table) {
$table->boolean('is_notification_notifications_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_notification_notifications_enabled');
});
}
};

View File

@ -15,12 +15,12 @@ class ApplicationSeeder extends Seeder
public function run(): void public function run(): void
{ {
Application::create([ Application::create([
'name' => 'coollabsio/coolify-examples:nodejs-fastify', 'name' => 'NodeJS Fastify Example',
'description' => 'NodeJS Fastify Example',
'fqdn' => 'http://nodejs.127.0.0.1.sslip.io', 'fqdn' => 'http://nodejs.127.0.0.1.sslip.io',
'repository_project_id' => 603035348, 'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples', 'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'nodejs-fastify', 'git_branch' => 'main',
'base_directory' => '/nodejs',
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => '3000', 'ports_exposes' => '3000',
'environment_id' => 1, 'environment_id' => 1,
@ -30,12 +30,12 @@ class ApplicationSeeder extends Seeder
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);
Application::create([ Application::create([
'name' => 'coollabsio/coolify-examples:dockerfile', 'name' => 'Dockerfile Example',
'description' => 'Dockerfile Example',
'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io', 'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io',
'repository_project_id' => 603035348, 'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples', 'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'dockerfile', 'git_branch' => 'main',
'base_directory' => '/dockerfile',
'build_pack' => 'dockerfile', 'build_pack' => 'dockerfile',
'ports_exposes' => '80', 'ports_exposes' => '80',
'environment_id' => 1, 'environment_id' => 1,
@ -45,8 +45,7 @@ class ApplicationSeeder extends Seeder
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);
Application::create([ Application::create([
'name' => 'pure-dockerfile', 'name' => 'Pure Dockerfile Example',
'description' => 'Pure Dockerfile Example',
'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io', 'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io',
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main', 'git_branch' => 'main',

View File

@ -419,7 +419,7 @@ const magicActions = [{
}, },
{ {
id: 24, id: 24,
name: 'Goto: Boarding process', name: 'Goto: Onboarding process',
icon: 'goto', icon: 'goto',
sequence: ['main', 'redirect'] sequence: ['main', 'redirect']
}, },
@ -667,7 +667,7 @@ async function redirect() {
targetUrl.pathname = `/team` targetUrl.pathname = `/team`
break; break;
case 24: case 24:
targetUrl.pathname = `/boarding` targetUrl.pathname = `/onboarding`
break; break;
case 25: case 25:
targetUrl.pathname = `/security/api-tokens` targetUrl.pathname = `/security/api-tokens`

View File

@ -150,7 +150,17 @@
</a> </a>
</li> </li>
@endif @endif
<li title="Notifications" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('notification.index') }}">
<svg class="{{ request()->is('notifications*') ? 'text-warning icon' : 'icon' }}"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" />
</svg>
Notifications
</a>
</li>
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<li title="Settings" class="hover:bg-coolgray-200"> <li title="Settings" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="/settings"> <a class="hover:bg-transparent hover:no-underline" href="/settings">
@ -167,14 +177,14 @@
</a> </a>
</li> </li>
@endif @endif
<li title="Boarding" class="hover:bg-coolgray-200"> <li title="Onboarding" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('boarding') }}"> <a class="hover:bg-transparent hover:no-underline" href="{{ route('onboarding') }}">
<svg class="{{ request()->is('boarding*') ? 'text-warning icon' : 'icon' }}" <svg class="{{ request()->is('onboarding*') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="M224 128a8 8 0 0 1-8 8h-88a8 8 0 0 1 0-16h88a8 8 0 0 1 8 8m-96-56h88a8 8 0 0 0 0-16h-88a8 8 0 0 0 0 16m88 112h-88a8 8 0 0 0 0 16h88a8 8 0 0 0 0-16M82.34 42.34L56 68.69L45.66 58.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 132.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 196.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" /> d="M224 128a8 8 0 0 1-8 8h-88a8 8 0 0 1 0-16h88a8 8 0 0 1 8 8m-96-56h88a8 8 0 0 0 0-16h-88a8 8 0 0 0 0 16m88 112h-88a8 8 0 0 0 0 16h88a8 8 0 0 0 0-16M82.34 42.34L56 68.69L45.66 58.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 132.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 196.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" />
</svg> </svg>
Boarding Onboarding
</a> </a>
</li> </li>
</div> </div>

View File

@ -0,0 +1,25 @@
<div class="pb-6">
<div class="flex items-end gap-2">
<h1>Team Notifications</h1>
</div>
<nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center">
<li>
<div class="flex items-center">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
</li>
</ol>
</nav>
<nav class="navbar-main">
<a class="{{ request()->routeIs('notification.index') ? 'text-white' : '' }}"
href="{{ route('notification.index') }}">
<button>General</button>
</a>
<div class="flex-1"></div>
<div class="-mt-9">
<livewire:switch-team />
</div>
</nav>
</div>

View File

@ -4,11 +4,13 @@
href="{{ route('server.proxy', $parameters) }}"> href="{{ route('server.proxy', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@if (data_get($server, 'proxy.type') !== 'NONE') @if ($server->proxyType() !== 'NONE')
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}" {{-- @if ($server->proxyType() === 'TRAEFIK_V2') --}}
href="{{ route('server.proxy.dynamic-confs', $parameters) }}"> <a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
<button>Dynamic Configurations</button> href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
</a> <button>Dynamic Configurations</button>
</a>
{{-- @endif --}}
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
href="{{ route('server.proxy.logs', $parameters) }}"> href="{{ route('server.proxy.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>

View File

@ -8,6 +8,6 @@
{{ str($status)->before(':')->headline() }} {{ str($status)->before(':')->headline() }}
</div> </div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('(')) @if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
<div class="text-xs text-success">({{ str($status)->after(':') }})</div> <div class="text-xs {{ str($status)->contains('unhealthy') ? 'text-warning' : 'text-success' }}">({{ str($status)->after(':') }})</div>
@endif @endif
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="pb-6"> <div class="pb-6">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Team</h1> <h1>Team</h1>
<a href="/team/new"><x-forms.button>+ Add Team</x-forms.button></a> <a href="/team/new"><x-forms.button>+ Add Team</x-forms.button></a>
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center"> <ol class="inline-flex items-center">
@ -17,18 +17,15 @@
<a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}"> <a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button> <button>General</button>
</a> </a>
<a class="{{ request()->routeIs('team.member.index') ? 'text-white' : '' }}" href="{{ route('team.member.index') }}"> <a class="{{ request()->routeIs('team.member.index') ? 'text-white' : '' }}"
href="{{ route('team.member.index') }}">
<button>Members</button> <button>Members</button>
</a> </a>
<a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}"
href="{{ route('team.storage.index') }}"> href="{{ route('team.storage.index') }}">
<button>S3 Storages</button> <button>S3 Storages</button>
</a> </a>
<a class="{{ request()->routeIs('team.notification.index') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('team.shared-variables.index') ? 'text-white' : '' }}"
href="{{ route('team.notification.index') }}">
<button>Notifications</button>
</a>
<a class="{{ request()->routeIs('team.shared-variables.index') ? 'text-white' : '' }}"
href="{{ route('team.shared-variables.index') }}"> href="{{ route('team.shared-variables.index') }}">
<button>Shared Variables</button> <button>Shared Variables</button>
</a> </a>

View File

@ -27,6 +27,7 @@
<ul x-data="{ <ul x-data="{
toasts: [], toasts: [],
toastsHovered: false, toastsHovered: false,
timeout: null,
expanded: false, expanded: false,
layout: 'default', layout: 'default',
position: 'top-center', position: 'top-center',
@ -286,14 +287,12 @@
} }
stackToasts(); stackToasts();
$watch('toastsHovered', function(value) { $watch('toastsHovered', function(value) {
if (layout == 'default') { if (layout == 'default') {
if (position.includes('bottom')) { if (position.includes('bottom')) {
resetBottom(); resetBottom();
} else { } else {
resetTop(); resetTop();
} }
if (value) { if (value) {
// calculate the new positions // calculate the new positions
expanded = true; expanded = true;
@ -319,13 +318,32 @@
<template x-for="(toast, index) in toasts" :key="toast.id"> <template x-for="(toast, index) in toasts" :key="toast.id">
<li :id="toast.id" x-data="{ <li :id="toast.id" x-data="{
toastHovered: false toastHovered: false,
}" x-init="if (position.includes('bottom')) { }" x-init="if (position.includes('bottom')) {
$el.firstElementChild.classList.add('toast-bottom'); $el.firstElementChild.classList.add('toast-bottom');
$el.firstElementChild.classList.add('opacity-0', 'translate-y-full'); $el.firstElementChild.classList.add('opacity-0', 'translate-y-full');
} else { } else {
$el.firstElementChild.classList.add('opacity-0', '-translate-y-full'); $el.firstElementChild.classList.add('opacity-0', '-translate-y-full');
} }
$watch('toastsHovered', function(value) {
if (value && this.timeout) {
clearTimeout(this.timeout);
} else {
this.timeout = setTimeout(function() {
setTimeout(function() {
$el.firstElementChild.classList.remove('opacity-100');
$el.firstElementChild.classList.add('opacity-0');
if (toasts.length == 1) {
$el.firstElementChild.classList.remove('translate-y-0');
$el.firstElementChild.classList.add('-translate-y-full');
}
setTimeout(function() {
deleteToastWithId(toast.id)
}, 300);
}, 5);
}, 2000)
}
});
setTimeout(function() { setTimeout(function() {
setTimeout(function() { setTimeout(function() {
@ -342,7 +360,7 @@
}, 5); }, 5);
}, 50); }, 50);
setTimeout(function() { this.timeout = setTimeout(function() {
setTimeout(function() { setTimeout(function() {
$el.firstElementChild.classList.remove('opacity-100'); $el.firstElementChild.classList.remove('opacity-100');
$el.firstElementChild.classList.add('opacity-0'); $el.firstElementChild.classList.add('opacity-0');
@ -390,12 +408,12 @@
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z"
fill="currentColor"></path> fill="currentColor"></path>
</svg> </svg>
<p class="leading-2 text-neutral-200" <p class="leading-2 text-neutral-200" x-html="toast.message">
x-html="toast.message">
</p> </p>
</div> </div>
<p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }" <p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
class="mt-1.5 text-xs leading-2 opacity-90 whitespace-pre-wrap" x-html="toast.description"></p> class="mt-1.5 text-xs leading-2 opacity-90 whitespace-pre-wrap"
x-html="toast.description"></p>
</div> </div>
</template> </template>
<template x-if="toast.html"> <template x-if="toast.html">

View File

@ -3,7 +3,7 @@
<div> <div>
@if ($currentState === 'welcome') @if ($currentState === 'welcome')
<h1 class="text-5xl font-bold">Welcome to Coolify</h1> <h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p> <p class="py-6 text-xl text-center">Let me help you set up the basics.</p>
<div class="flex justify-center "> <div class="flex justify-center ">
<x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get <x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get
Started Started
@ -24,12 +24,12 @@
<x-highlighted text="Self-hosting with superpowers!" /></span> <x-highlighted text="Self-hosting with superpowers!" /></span>
</x-slot:question> </x-slot:question>
<x-slot:explanation> <x-slot:explanation>
<p><x-highlighted text="Task automation:" /> You do not to manage your servers too much. Coolify do <p><x-highlighted text="Task automation:" /> You don't need to manage your servers anymore. Coolify does
it for you.</p> it for you.</p>
<p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your server, so <p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your servers, so
everything works without Coolify (except integrations and automations).</p> everything works without a connection to Coolify (except integrations and automations).</p>
<p><x-highlighted text="Monitoring:" />You will get notified on your favourite platform (Discord, <p><x-highlighted text="Monitoring:" />You can get notified on your favourite platforms (Discord,
Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p> Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.</p>
</x-slot:explanation> </x-slot:explanation>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:click="explanation">Next <x-forms.button class="justify-center w-64 box" wire:click="explanation">Next
@ -40,8 +40,8 @@
@if ($currentState === 'select-server-type') @if ($currentState === 'select-server-type')
<x-boarding-step title="Server"> <x-boarding-step title="Server">
<x-slot:question> <x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" /> Do you want to deploy your resources to your <x-highlighted text="Localhost" />
or on a <x-highlighted text="Remote Server" />? or to a <x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:target="setServerType('localhost')" <x-forms.button class="justify-center w-64 box" wire:target="setServerType('localhost')"
@ -297,12 +297,12 @@
You already have some projects. Do you want to use one of them or should I create a new one for You already have some projects. Do you want to use one of them or should I create a new one for
you? you?
@else @else
I will create an initial project for you. You can change all the details later on. Let's create an initial project for you. You can change all the details later on.
@endif @endif
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Let's create a new <x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Create new
one!</x-forms.button> project!</x-forms.button>
<div> <div>
@if (count($projects) > 0) @if (count($projects) > 0)
<form wire:submit='selectExistingProject' class="flex flex-col w-full gap-4 lg:w-96"> <form wire:submit='selectExistingProject' class="flex flex-col w-full gap-4 lg:w-96">
@ -319,9 +319,9 @@
</div> </div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Projects are bound together several resources into one virtual group. There are no <p>Projects contain several resources combined into one virtual group. There are no
limitations on the number of projects you could have.</p> limitations on the number of projects you can add.</p>
<p>Each project should have at least one environment. This helps you to create a production & <p>Each project should have at least one environment, this allows you to create a production &
staging version of the same application, but grouped separately.</p> staging version of the same application, but grouped separately.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@ -331,7 +331,7 @@
@if ($currentState === 'create-resource') @if ($currentState === 'create-resource')
<x-boarding-step title="Resources"> <x-boarding-step title="Resources">
<x-slot:question> <x-slot:question>
I will redirect you to the new resource page, where you can create your first resource. Let's go to the new resource page, where you can create your first resource.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do <div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do

View File

@ -17,7 +17,7 @@
@endif @endif
@if ($projects->count() === 0 && $servers->count() === 0) @if ($projects->count() === 0 && $servers->count() === 0)
No resources found. Add your first server & private key <a class="text-white underline" No resources found. Add your first server & private key <a class="text-white underline"
href="{{ route('server.create') }}">here</a> or go to the <a class="text-white underline" href="{{ route('boarding') }}">boarding page</a>. href="{{ route('server.create') }}">here</a> or go to the <a class="text-white underline" href="{{ route('onboarding') }}">onboarding page</a>.
@endif @endif
@if ($projects->count() > 0) @if ($projects->count() > 0)
<h3 class="pb-4">Projects</h3> <h3 class="pb-4">Projects</h3>

View File

@ -5,9 +5,7 @@
<div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div> <div class="text-5xl font-bold tracking-tight text-center text-white">Coolify</div>
</a> </a>
</div> </div>
<div class="flex items-center justify-center pb-4 text-center">
Set your initial password
</div>
<form class="flex flex-col gap-2" wire:submit='submit'> <form class="flex flex-col gap-2" wire:submit='submit'>
<x-forms.input id="email" type="email" placeholder="Email" readonly label="Email" /> <x-forms.input id="email" type="email" placeholder="Email" readonly label="Email" />
<x-forms.input id="password" type="password" placeholder="New Password" label="New Password" required /> <x-forms.input id="password" type="password" placeholder="New Password" label="New Password" required />

View File

@ -7,7 +7,8 @@
consider donating!</a>💜</span> consider donating!</a>💜</span>
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and <span>It enables us to keep creating features without paywalls, ensuring our work remains free and
open.</span> open.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disable'>Disable This Popup</x-forms.button> <x-forms.button class="bg-coolgray-400" wire:click='disableSponsorship'>Disable This
Popup</x-forms.button>
</div> </div>
</div> </div>
@endif @endif
@ -20,4 +21,16 @@
</div> </div>
</x-banner> </x-banner>
@endif @endif
@if (!currentTeam()->isAnyNotificationEnabled())
<div class="toast">
<div class="flex flex-col text-white rounded alert bg-coolgray-200">
<span><span class="font-bold text-red-500">WARNING:</span> No notifications enabled.<br><br> It is highly recommended to enable at least
one
notification channel to receive important alerts.<br>Visit <a href="{{ route('notification.index') }}"
class="text-white underline">/notification</a> to enable notifications.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disableNotifications'>Disable This
Popup</x-forms.button>
</div>
</div>
@endif
</div> </div>

View File

@ -6,13 +6,22 @@
<h2>General</h2> <h2>General</h2>
<x-forms.button type="submit" label="Save">Save</x-forms.button> <x-forms.button type="submit" label="Save">Save</x-forms.button>
</div> </div>
<div class="flex gap-2"> <div class="flex flex-col gap-2 lg:flex-row">
<x-forms.input id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="email" label="Email" readonly /> <x-forms.input id="email" label="Email" readonly />
</div> </div>
</form> </form>
<h2 class="py-4">Subscription</h2> <form wire:submit='resetPassword' class="flex flex-col max-w-xl pt-4">
<a href="{{ route('team.index') }}">Check in Team Settings</a> <div class="flex items-center gap-2">
<h2>Reset Password</h2>
<x-forms.button type="submit" label="Save">Reset</x-forms.button>
</div>
<div class="flex flex-col gap-2">
<x-forms.input id="current_password" label="Current Password" required type="password" />
<x-forms.input id="new_password" label="New Password" required type="password" />
<x-forms.input id="new_password_confirmation" label="New Password Again" required type="password" />
</div>
</form>
<h2 class="py-4">Two-factor Authentication</h2> <h2 class="py-4">Two-factor Authentication</h2>
@if (session('status') == 'two-factor-authentication-enabled') @if (session('status') == 'two-factor-authentication-enabled')
<div class="mb-4 font-medium"> <div class="mb-4 font-medium">

View File

@ -45,11 +45,11 @@
</div> </div>
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled" <x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled"
label="Raw Compose Deployment" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" /> helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" />
</div> </div>
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) @if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service) @foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image'))) @if (!isDatabaseImage(data_get($service, 'image')))
@ -210,10 +210,10 @@
id="application.custom_docker_run_options" label="Custom Docker Options" /> id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif @endif
@else @else
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='text-white underline' href='https://coolify.io/docs/custom-docker-options'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='text-white underline' href='https://coolify.io/docs/custom-docker-options'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" /> id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@ -250,6 +250,21 @@
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea> <x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button> <x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
@endif @endif
<h3 class="pt-8">Pre/Post Deployment Commands</h3>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.pre_deployment_command" label="Pre-deployment Command"
helper="An optional script or command to execute in the existing container before the deployment begins." />
<x-forms.input id="application.pre_deployment_command_container" label="Container Name"
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="php artisan migrate" id="application.post_deployment_command"
label="Post-deployment Command"
helper="An optional script or command to execute in the newly built container after the deployment completes." />
<x-forms.input id="application.post_deployment_command_container" label="Container Name"
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -13,7 +13,10 @@
</x-modal> </x-modal>
<div class="pt-6"> <div class="pt-6">
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" /> <livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<h3 class="py-4">Executions</h3> <div class="flex items-center gap-2">
<h3 class="py-4">Executions</h3>
<x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button>
</div>
<livewire:project.database.backup-executions :backup="$backup" :executions="$executions" /> <livewire:project.database.backup-executions :backup="$backup" :executions="$executions" />
</div> </div>
</div> </div>

View File

@ -21,8 +21,8 @@
<div class='text-helper'>git@..</div> <div class='text-helper'>git@..</div>
</div> </div>
<div class="flex gap-1"> <div class="flex gap-1">
<div>Preselect branch (eg: static):</div> <div>Preselect branch (eg: main):</div>
<div class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/static</div> <div class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/main</div>
</div> </div>
<div> <div>
For example application deployments, checkout <a class="text-white underline" For example application deployments, checkout <a class="text-white underline"

View File

@ -289,10 +289,10 @@
</div> </div>
@endforelse @endforelse
</div> </div>
@if ($isDatabase) {{-- @if ($isDatabase)
<div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will <div class="text-center">Swarm clusters are excluded from this type of resource at the moment. It will
be activated soon. Stay tuned.</div> be activated soon. Stay tuned.</div>
@endif @endif --}}
@endif @endif
@if ($current_step === 'destinations') @if ($current_step === 'destinations')
<ul class="pb-10 steps"> <ul class="pb-10 steps">

View File

@ -16,7 +16,7 @@
</div> </div>
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave id="service.connect_to_docker_network" label="Connect To Predefined Network" <x-forms.checkbox instantSave id="service.connect_to_docker_network" label="Connect To Predefined Network"
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='text-white underline' href='https://coolify.io/docs/docker/compose#connect-to-predefined-networks'>this</a>." /> helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='text-white underline' target='_blank' href='https://coolify.io/docs/docker/compose#connect-to-predefined-networks'>this</a>." />
</div> </div>
@if ($fields) @if ($fields)
<div> <div>

View File

@ -17,7 +17,7 @@
</div> </div>
<div>Environment variables (secrets) for this resource.</div> <div>Environment variables (secrets) for this resource.</div>
@if ($resource->type() === 'service') @if ($resource->type() === 'service')
<div>If you cannot find a variable here, or need a new one, define it in the Docker Compose file.</div> <div>Hardcoded variables are not shown here.</div>
@endif @endif
</div> </div>
@if ($view === 'normal') @if ($view === 'normal')

View File

@ -1,16 +1,29 @@
<div> <div>
@if (data_get($server, 'proxy.type')) @if ($server->proxyType())
<div x-init="$wire.loadProxyConfiguration"> <div x-init="$wire.loadProxyConfiguration">
@if ($selectedProxy === 'TRAEFIK_V2') @if ($selectedProxy !== 'NONE')
<form wire:submit='submit'> <form wire:submit='submit'>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Configuration</h2> <h2>Configuration</h2>
<x-forms.button type="submit">Save</x-forms.button>
@if ($server->proxy->status === 'exited') @if ($server->proxy->status === 'exited')
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button> <x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@else
<x-forms.button disabled wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@endif @endif
<x-forms.button type="submit">Save</x-forms.button>
</div> </div>
<div class="pt-3 pb-4 ">Traefik v2</div> <div class="pb-4 "> <svg class="inline-flex w-6 h-6 mr-2 text-warning" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>Before switching proxies, please read <a class="text-white underline"
href="https://coolify.io/docs/server/switching-proxies">this</a>.</div>
@if ($server->proxyType() === 'TRAEFIK_V2')
<div class="pb-4">Traefik v2</div>
@elseif ($server->proxyType() === 'CADDY')
<div class="pb-4 ">Caddy</div>
@endif
@if ( @if (
$server->proxy->last_applied_settings && $server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings) $server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
@ -26,7 +39,7 @@
<div wire:loading.remove wire:target="loadProxyConfiguration"> <div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxy_settings) @if ($proxy_settings)
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<x-forms.textarea label="Configuration file: traefik.conf" name="proxy_settings" <x-forms.textarea label="Configuration file" name="proxy_settings"
wire:model="proxy_settings" rows="30" /> wire:model="proxy_settings" rows="30" />
<x-forms.button wire:click.prevent="reset_proxy_configuration"> <x-forms.button wire:click.prevent="reset_proxy_configuration">
Reset configuration to default Reset configuration to default
@ -40,7 +53,7 @@
<h2>Configuration</h2> <h2>Configuration</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button> <x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div> </div>
<div class="pt-3 pb-4">Custom (None) Proxy Selected</div> <div class="pt-2 pb-4">Custom (None) Proxy Selected</div>
@else @else
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Configuration</h2> <h2>Configuration</h2>
@ -57,14 +70,13 @@
</x-forms.button> </x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('TRAEFIK_V2')"> <x-forms.button class="box" wire:click="select_proxy('TRAEFIK_V2')">
Traefik Traefik
v2 </x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('CADDY')">
Caddy (experimental)
</x-forms.button> </x-forms.button>
<x-forms.button disabled class="box"> <x-forms.button disabled class="box">
Nginx Nginx
</x-forms.button> </x-forms.button>
<x-forms.button disabled class="box">
Caddy
</x-forms.button>
</div> </div>
</div> </div>
@endif @endif

View File

@ -16,10 +16,10 @@
</p> </p>
</x-slot:modalBody> </x-slot:modalBody>
</x-modal> </x-modal>
@if ($server->isFunctional() && data_get($server, 'proxy.type') !== 'NONE') @if ($server->isFunctional() && $server->proxyType() !== 'NONE')
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-4"> <div class="flex gap-4">
@if ($currentRoute === 'server.proxy' && $traefikDashboardAvailable) @if ($currentRoute === 'server.proxy' && $traefikDashboardAvailable && $server->proxyType() === 'TRAEFIK_V2')
<button> <button>
<a target="_blank" href="http://{{ $serverIp }}:8080"> <a target="_blank" href="http://{{ $serverIp }}:8080">
Traefik Dashboard Traefik Dashboard

View File

@ -19,7 +19,7 @@
Add</button> Add</button>
</x-slide-over> </x-slide-over>
</div> </div>
<div class='pb-4'>You can add dynamic Traefik configurations here.</div> <div class='pb-4'>You can add dynamic proxy configurations here.</div>
</div> </div>
</div> </div>
<div wire:loading wire:target="loadDynamicConfigurations"> <div wire:loading wire:target="loadDynamicConfigurations">
@ -29,12 +29,12 @@
@if ($contents?->isNotEmpty()) @if ($contents?->isNotEmpty())
@foreach ($contents as $fileName => $value) @foreach ($contents as $fileName => $value)
<div class="flex flex-col gap-2 py-2"> <div class="flex flex-col gap-2 py-2">
@if (str_replace('|', '.', $fileName) === 'coolify.yaml') @if (str_replace('|', '.', $fileName) === 'coolify.yaml' || str_replace('|', '.', $fileName) === 'Caddyfile' || str_replace('|', '.', $fileName) === 'coolify.caddy' || str_replace('|', '.', $fileName) === 'default_redirect_404.caddy')
<div> <div>
<h3 class="text-white">File: {{ str_replace('|', '.', $fileName) }}</h3> <h3 class="text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
</div> </div>
<x-forms.textarea disabled name="proxy_settings" <x-forms.textarea disabled name="proxy_settings"
wire:model="contents.{{ $fileName }}" rows="10" /> wire:model="contents.{{ $fileName }}" rows="5" />
@else @else
<livewire:server.proxy.dynamic-configuration-navbar :server_id="$server->id" <livewire:server.proxy.dynamic-configuration-navbar :server_id="$server->id"
:fileName="$fileName" :value="$value" :newFile="false" :fileName="$fileName" :value="$value" :newFile="false"

View File

@ -1,5 +1,5 @@
<form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col gap-4"> <form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col gap-4">
<x-forms.input id="fileName" label="Filename (.yaml or .yml)" required /> <x-forms.input id="fileName" label="Filename" required />
<x-forms.textarea id="value" label="Configuration" required rows="20" /> <x-forms.textarea id="value" label="Configuration" required rows="20" />
<x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button> <x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button>
</form> </form>

View File

@ -45,7 +45,8 @@
{{ data_get($resource, 'environment.name') }} {{ data_get($resource, 'environment.name') }}
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap"><a class="" <td class="px-5 py-4 text-sm whitespace-nowrap"><a class=""
href="{{ $resource->link() }}">{{ $resource->name }} <x-internal-link/></a> href="{{ $resource->link() }}">{{ $resource->name }}
<x-internal-link /></a>
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ str($resource->type())->headline() }}</td> {{ str($resource->type())->headline() }}</td>
@ -138,6 +139,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -69,7 +69,7 @@
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" /> <x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div> </div>
<div class="flex items-end gap-2 "> <div class="flex items-end gap-2 ">
<h3 class="pt-4">Permissions</h3> <h2 class="pt-4">Permissions</h2>
<x-forms.button wire:click.prevent="checkPermissions">Refetch</x-forms.button> <x-forms.button wire:click.prevent="checkPermissions">Refetch</x-forms.button>
<a href="{{ get_permissions_path($github_app) }}"> <a href="{{ get_permissions_path($github_app) }}">
<x-forms.button> <x-forms.button>
@ -93,6 +93,57 @@
</div> </div>
@endif @endif
</form> </form>
<div class="w-full pt-10">
<div class="h-full">
<div class="flex flex-col">
<div class="flex gap-2">
<h2>Resources</h2>
</div>
<div class="pb-4 title">Here you can find all resources that are used by this source.</div>
</div>
<div class="flex flex-col">
<div class="flex flex-col">
<div class="overflow-x-auto">
<div class="inline-block min-w-full">
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-coolgray-400">
<thead>
<tr class="text-neutral-500">
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Project
</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
Environment</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Type</th>
</tr>
</thead>
<tbody class="divide-y divide-coolgray-400">
@forelse ($applications->sortBy('name',SORT_NATURAL) as $resource)
<tr class="text-white bg-coolblack hover:bg-coolgray-100">
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource->project(), 'name') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'environment.name') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap"><a class=""
href="{{ $resource->link() }}">{{ $resource->name }}
<x-internal-link /></a>
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ str($resource->type())->headline() }}</td>
</tr>
@empty
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@else @else
<div class="flex items-center gap-2 pb-4"> <div class="flex items-center gap-2 pb-4">
<h1>GitHub App</h1> <h1>GitHub App</h1>

View File

@ -1,5 +1,5 @@
<div> <div>
<x-team.navbar /> <x-notifications.navbar />
<h2 class="pb-4">Notifications</h2> <h2 class="pb-4">Notifications</h2>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'email' }" class="flex h-full"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'email' }" class="flex h-full">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">

View File

@ -1,8 +1,10 @@
<?php <?php
use App\Http\Controllers\Api\Deploy; use App\Http\Controllers\Api\Deploy;
use App\Http\Controllers\Api\Project; use App\Http\Controllers\Api\Domains;
use App\Http\Controllers\Api\Resources;
use App\Http\Controllers\Api\Server; use App\Http\Controllers\Api\Server;
use App\Http\Controllers\Api\Team;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -29,11 +31,24 @@ Route::group([
return response(config('version')); return response(config('version'));
}); });
Route::get('/deploy', [Deploy::class, 'deploy']); Route::get('/deploy', [Deploy::class, 'deploy']);
Route::get('/deployments', [Deploy::class, 'deployments']);
Route::get('/servers', [Server::class, 'servers']); Route::get('/servers', [Server::class, 'servers']);
Route::get('/server/{uuid}', [Server::class, 'server_by_uuid']); Route::get('/server/{uuid}', [Server::class, 'server_by_uuid']);
Route::get('/projects', [Project::class, 'projects']);
Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']); Route::get('/resources', [Resources::class, 'resources']);
Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']); Route::get('/domains', [Domains::class, 'domains']);
Route::get('/teams', [Team::class, 'teams']);
Route::get('/team/current', [Team::class, 'current_team']);
Route::get('/team/current/members', [Team::class, 'current_team_members']);
Route::get('/team/{id}', [Team::class, 'team_by_id']);
Route::get('/team/{id}/members', [Team::class, 'members_by_id']);
//Route::get('/projects', [Project::class, 'projects']);
//Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']);
//Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']);
}); });
Route::get('/{any}', function () { Route::get('/{any}', function () {

View File

@ -85,7 +85,7 @@ if (isDev()) {
Route::get('/admin', AdminIndex::class)->name('admin.index'); Route::get('/admin', AdminIndex::class)->name('admin.index');
Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot'); Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot');
Route::get('/api/v1/test/realtime', [Controller::class, 'realtime_test'])->middleware('auth'); Route::get('/realtime', [Controller::class, 'realtime_test'])->middleware('auth');
Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index');
Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email'); Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email');
Route::get('/email/verify/{id}/{hash}', [Controller::class, 'email_verify'])->middleware(['auth'])->name('verify.verify'); Route::get('/email/verify/{id}/{hash}', [Controller::class, 'email_verify'])->middleware(['auth'])->name('verify.verify');
@ -108,7 +108,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
}); });
Route::get('/', Dashboard::class)->name('dashboard'); Route::get('/', Dashboard::class)->name('dashboard');
Route::get('/boarding', BoardingIndex::class)->name('boarding'); Route::get('/onboarding', BoardingIndex::class)->name('onboarding');
Route::get('/subscription', SubscriptionShow::class)->name('subscription.show'); Route::get('/subscription', SubscriptionShow::class)->name('subscription.show');
Route::get('/subscription/new', SubscriptionIndex::class)->name('subscription.index'); Route::get('/subscription/new', SubscriptionIndex::class)->name('subscription.index');
@ -121,11 +121,13 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/', TagsIndex::class)->name('tags.index'); Route::get('/', TagsIndex::class)->name('tags.index');
Route::get('/{tag_name}', TagsShow::class)->name('tags.show'); Route::get('/{tag_name}', TagsShow::class)->name('tags.show');
}); });
Route::prefix('notifications')->group(function () {
Route::get('/', TeamNotificationIndex::class)->name('notification.index');
});
Route::prefix('team')->group(function () { Route::prefix('team')->group(function () {
Route::get('/', TeamIndex::class)->name('team.index'); Route::get('/', TeamIndex::class)->name('team.index');
Route::get('/new', TeamCreate::class)->name('team.create'); Route::get('/new', TeamCreate::class)->name('team.create');
Route::get('/members', TeamMemberIndex::class)->name('team.member.index'); Route::get('/members', TeamMemberIndex::class)->name('team.member.index');
Route::get('/notifications', TeamNotificationIndex::class)->name('team.notification.index');
Route::get('/shared-variables', TeamSharedVariablesIndex::class)->name('team.shared-variables.index'); Route::get('/shared-variables', TeamSharedVariablesIndex::class)->name('team.shared-variables.index');
Route::get('/storages', TeamStorageIndex::class)->name('team.storage.index'); Route::get('/storages', TeamStorageIndex::class)->name('team.storage.index');
Route::get('/storages/new', TeamStorageCreate::class)->name('team.storage.create'); Route::get('/storages/new', TeamStorageCreate::class)->name('team.storage.create');

View File

@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
VERSION="1.2.2" VERSION="1.2.3"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@ -122,6 +122,16 @@ if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################" echo "###############################################################################"
fi fi
# Detect if docker is installed via snap
if [ -x "$(command -v snap)" ]; then
if snap list | grep -q docker; then
echo "Docker is installed via snap."
echo "Please note that Coolify does not support Docker installed via snap."
echo "Please remove Docker with snap (snap remove docker) and reexecute this script."
exit 1
fi
fi
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
if [ "$OS_TYPE" == 'almalinux' ]; then if [ "$OS_TYPE" == 'almalinux' ]; then
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo

View File

@ -2,6 +2,7 @@
# slogan: Website change detection monitor and notifications. # slogan: Website change detection monitor and notifications.
# tags: web, alert, monitor # tags: web, alert, monitor
# logo: svgs/changedetection.png # logo: svgs/changedetection.png
# port: 5000
services: services:
changedetection: changedetection:
@ -9,7 +10,7 @@ services:
volumes: volumes:
- changedetection-data:/datastore - changedetection-data:/datastore
environment: environment:
- SERVICE_FQDN_CHANGEDETECTION - SERVICE_FQDN_CHANGEDETECTION_5000
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- BASE_URL=$SERVICE_FQDN_CHANGEDETECTION - BASE_URL=$SERVICE_FQDN_CHANGEDETECTION

View File

@ -2,12 +2,13 @@
# slogan: Code-Server is a web-based code editor that enables remote coding and collaboration from any device, anywhere. # slogan: Code-Server is a web-based code editor that enables remote coding and collaboration from any device, anywhere.
# tags: code, editor, remote, collaboration # tags: code, editor, remote, collaboration
# logo: svgs/code-server.svg # logo: svgs/code-server.svg
# port: 8443
services: services:
code-server: code-server:
image: lscr.io/linuxserver/code-server:latest image: lscr.io/linuxserver/code-server:latest
environment: environment:
- SERVICE_FQDN_CODESERVER - SERVICE_FQDN_CODESERVER_8443
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=Europe/Madrid - TZ=Europe/Madrid

View File

@ -1,12 +1,13 @@
# documentation: https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard # documentation: https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard
# slogan: A dashboard, inspired by SUI. # slogan: A dashboard, inspired by SUI.
# tags: dashboard, web, search, bookmarks # tags: dashboard, web, search, bookmarks
# port: 8080
services: services:
dashboard: dashboard:
image: phntxx/dashboard:latest image: phntxx/dashboard:latest
environment: environment:
- SERVICE_FQDN_DASHBOARD - SERVICE_FQDN_DASHBOARD_8080
volumes: volumes:
- dashboard-data:/app/data - dashboard-data:/app/data
healthcheck: healthcheck:

View File

@ -2,6 +2,7 @@
# slogan: Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content. # slogan: Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.
# tags: directus, cms, database, sql # tags: directus, cms, database, sql
# logo: svgs/directus.svg # logo: svgs/directus.svg
# port: 8055
services: services:
directus: directus:
@ -10,7 +11,7 @@ services:
- directus-uploads:/directus/uploads - directus-uploads:/directus/uploads
- directus-extensions:/directus/extensions - directus-extensions:/directus/extensions
environment: environment:
- SERVICE_FQDN_DIRECTUS - SERVICE_FQDN_DIRECTUS_8055
- KEY=$SERVICE_BASE64_64_KEY - KEY=$SERVICE_BASE64_64_KEY
- SECRET=$SERVICE_BASE64_64_SECRET - SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}

View File

@ -2,15 +2,17 @@
# slogan: Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content. # slogan: Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.
# tags: directus, cms, database, sql # tags: directus, cms, database, sql
# logo: svgs/directus.svg # logo: svgs/directus.svg
# port: 8055
services: services:
directus: directus:
image: directus/directus:10.7 image: directus/directus:10
volumes: volumes:
- directus-database:/directus/database
- directus-uploads:/directus/uploads - directus-uploads:/directus/uploads
- directus-database:/directus/database
- directus-extensions:/directus/extensions
environment: environment:
- SERVICE_FQDN_DIRECTUS - SERVICE_FQDN_DIRECTUS_8055
- KEY=$SERVICE_BASE64_64_KEY - KEY=$SERVICE_BASE64_64_KEY
- SECRET=$SERVICE_BASE64_64_SECRET - SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}

View File

@ -2,12 +2,13 @@
# slogan: The Docker Registry is lets you distribute Docker images. # slogan: The Docker Registry is lets you distribute Docker images.
# tags: registry,images,docker # tags: registry,images,docker
# logo: svgs/docker-registry.png # logo: svgs/docker-registry.png
# port: 5000
services: services:
registry: registry:
image: registry:2 image: registry:2
environment: environment:
- SERVICE_FQDN_REGISTRY - SERVICE_FQDN_REGISTRY_5000
- REGISTRY_AUTH=htpasswd - REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry - REGISTRY_AUTH_HTPASSWD_REALM=Registry
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.password - REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.password

View File

@ -2,12 +2,13 @@
# slogan: Duplicati is a backup solution, allowing you to make scheduled backups with encryption. # slogan: Duplicati is a backup solution, allowing you to make scheduled backups with encryption.
# tags: backup, encryption # tags: backup, encryption
# logo: svgs/duplicati.webp # logo: svgs/duplicati.webp
# port: 8200
services: services:
duplicati: duplicati:
image: lscr.io/linuxserver/duplicati:latest image: lscr.io/linuxserver/duplicati:latest
environment: environment:
- SERVICE_FQDN_DUPLICATI - SERVICE_FQDN_DUPLICATI_8200
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=Europe/Madrid - TZ=Europe/Madrid

View File

@ -2,12 +2,13 @@
# slogan: A media server software that allows you to organize, stream, and access your multimedia content effortlessly. # slogan: A media server software that allows you to organize, stream, and access your multimedia content effortlessly.
# tags: media, server, movies, tv, music # tags: media, server, movies, tv, music
# logo: svgs/emby.png # logo: svgs/emby.png
# port: 8096
services: services:
emby: emby:
image: lscr.io/linuxserver/emby:latest image: lscr.io/linuxserver/emby:latest
environment: environment:
- SERVICE_FQDN_EMBY - SERVICE_FQDN_EMBY_8096
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=Europe/Madrid - TZ=Europe/Madrid

View File

@ -1,12 +1,13 @@
# documentation: https://github.com/mregni/EmbyStat # documentation: https://github.com/mregni/EmbyStat
# slogan: EmnyStat is a web analytics tool, designed to provide insight into website traffic and user behavior. # slogan: EmnyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.
# tags: media, server, movies, tv, music # tags: media, server, movies, tv, music
# port: 6555
services: services:
embystat: embystat:
image: lscr.io/linuxserver/embystat:latest image: lscr.io/linuxserver/embystat:latest
environment: environment:
- SERVICE_FQDN_EMBYSTAT - SERVICE_FQDN_EMBYSTAT_6555
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=Europe/Madrid - TZ=Europe/Madrid

View File

@ -2,12 +2,13 @@
# slogan: Fider is a feedback platform for collecting and managing user feedback. # slogan: Fider is a feedback platform for collecting and managing user feedback.
# tags: feedback, user-feedback # tags: feedback, user-feedback
# logo: svgs/fider.svg # logo: svgs/fider.svg
# port: 3000
services: services:
fider: fider:
image: getfider/fider:stable image: getfider/fider:stable
environment: environment:
BASE_URL: $SERVICE_FQDN_FIDER BASE_URL: $SERVICE_FQDN_FIDER_3000
DATABASE_URL: postgres://$SERVICE_USER_MYSQL:$SERVICE_PASSWORD_MYSQL@database:5432/fider?sslmode=disable DATABASE_URL: postgres://$SERVICE_USER_MYSQL:$SERVICE_PASSWORD_MYSQL@database:5432/fider?sslmode=disable
JWT_SECRET: $SERVICE_PASSWORD_64_FIDER JWT_SECRET: $SERVICE_PASSWORD_64_FIDER
EMAIL_NOREPLY: ${EMAIL_NOREPLY:-noreply@example.com} EMAIL_NOREPLY: ${EMAIL_NOREPLY:-noreply@example.com}

View File

@ -2,12 +2,13 @@
# slogan: A personal finances manager that can help you save money. # slogan: A personal finances manager that can help you save money.
# tags: finance, money, personal, manager # tags: finance, money, personal, manager
# logo: svgs/firefly.svg # logo: svgs/firefly.svg
# port: 8080
services: services:
firefly: firefly:
image: fireflyiii/core:latest image: fireflyiii/core:latest
environment: environment:
- SERVICE_FQDN_FIREFLY - SERVICE_FQDN_FIREFLY_8080
- APP_KEY=$SERVICE_BASE64_APPKEY - APP_KEY=$SERVICE_BASE64_APPKEY
- DB_HOST=mysql - DB_HOST=mysql
- DB_PORT=3306 - DB_PORT=3306

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