Merge branch 'main' into supabase

This commit is contained in:
Eirik Mo 2024-03-14 19:21:02 +01:00 committed by GitHub
commit 8c0c22a925
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 @@ public function __construct(CoolifyTaskArgs $remoteProcessArgs)
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);
$this->activity->refresh();
return $this->activity;

View File

@ -21,6 +21,8 @@ class RunRemoteProcess
public $call_event_on_finish = null;
public $call_event_data = null;
protected $time_start;
protected $current_time;
@ -34,7 +36,7 @@ class RunRemoteProcess
/**
* 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) {
@ -45,6 +47,7 @@ public function __construct(Activity $activity, bool $hide_from_output = false,
$this->hide_from_output = $hide_from_output;
$this->ignore_errors = $ignore_errors;
$this->call_event_on_finish = $call_event_on_finish;
$this->call_event_data = $call_event_data;
}
public static function decodeOutput(?Activity $activity = null): string
@ -111,9 +114,15 @@ public function __invoke(): ProcessResult
}
if ($this->call_event_on_finish) {
try {
if ($this->call_event_data) {
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) {
ray($e);
}

View File

@ -11,7 +11,12 @@ class CheckConfiguration
use AsAction;
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([
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml",

View File

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

View File

@ -15,7 +15,7 @@ public function handle(Server $server, ?string $proxy_settings = null)
if (is_null($proxy_settings)) {
$proxy_settings = CheckConfiguration::run($server, true);
}
$proxy_path = get_proxy_path();
$proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;

View File

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

View File

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

View File

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

View File

@ -100,6 +100,12 @@ private function process_file($file)
} else {
$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);
$yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [
@ -111,6 +117,9 @@ private function process_file($file)
'logo' => $logo,
'minversion' => $minversion,
];
if ($port) {
$payload['port'] = $port;
}
if ($env_file) {
$env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
$env_file_base64 = base64_encode($env_file_content);

View File

@ -21,6 +21,7 @@ public function __construct(
public ?string $status = null ,
public bool $ignore_errors = false,
public $call_event_on_finish = null,
public $call_event_data = null
) {
if(is_null($status)){
$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\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2;
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)
{
$teamId = get_team_id_from_token();
@ -27,7 +47,7 @@ public function deploy(Request $request)
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)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
return invalid_token();
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
@ -44,16 +64,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false)
if (count($uuids) === 0) {
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) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
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) {
return response()->json(['message' => $message->toArray()], 200);
}
if ($deployments->count() > 0) {
$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);
}
@ -66,10 +92,12 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (!$found_tag) {
$message->push("Tag {$tag} not found.");
// $message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications()->get();
@ -79,83 +107,78 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
continue;
}
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);
}
foreach ($services as $resource) {
$return_message = $this->deploy_resource($resource, $force);
['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
ray($message);
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') {
return $message->push("Resource ($resource) not found.");
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
$type = $resource?->getMorphClass();
if ($type === 'App\Models\Application') {
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $resource,
deployment_uuid: new Cuid2(7),
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
);
$message->push("Application {$resource->name} deployment queued.");
$message = "Application {$resource->name} deployment queued.";
} else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
$message = "Database {$resource->name} started.";
} else if ($type === 'App\Models\Service') {
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 @@ public function projects(Request $request)
{
$teamId = get_team_id_from_token();
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();
return response()->json($projects);
@ -21,7 +21,7 @@ public function project_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
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']);
return response()->json($project);
@ -30,7 +30,7 @@ public function environment_details(Request $request)
{
$teamId = get_team_id_from_token();
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();
$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 @@ public function servers(Request $request)
{
$teamId = get_team_id_from_token();
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) {
$server['is_reachable'] = $server->settings->is_reachable;
@ -26,7 +26,7 @@ public function server_by_uuid(Request $request)
$with_resources = $request->query('resources');
$teamId = get_team_id_from_token();
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();
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 @@ public function handle(Request $request, Closure $next): Response
}
if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect()->route('boarding');
return redirect()->route('onboarding');
}
return $next($request);
}
@ -43,7 +43,7 @@ public function handle(Request $request, Closure $next): Response
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect()->route('boarding');
return redirect()->route('onboarding');
}
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect(RouteServiceProvider::HOME);

View File

@ -24,6 +24,7 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
use Spatie\Url\Url;
@ -92,6 +93,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $buildTarget = null;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
private bool $custom_healthcheck_found = false;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
@ -239,6 +241,7 @@ public function handle(): void
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(false);
$this->run_post_deployment_command();
return;
} else if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
@ -273,6 +276,7 @@ public function handle(): void
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);
} catch (Exception $e) {
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
@ -294,13 +298,13 @@ public function handle(): void
"ignore_errors" => true,
]
);
$this->execute_remote_command(
[
"docker image prune -f >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
]
);
// $this->execute_remote_command(
// [
// "docker image prune -f >/dev/null 2>&1",
// "hidden" => true,
// "ignore_errors" => true,
// ]
// );
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
@ -456,6 +460,7 @@ private function deploy_dockerfile_buildpack()
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
@ -467,7 +472,6 @@ private function deploy_dockerfile_buildpack()
return;
}
}
$this->clone_repository();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
@ -775,7 +779,7 @@ private function health_check()
if ($this->server->isSwarm()) {
// Implement healthcheck for swarm
} else {
if ($this->application->isHealthcheckDisabled()) {
if ($this->application->isHealthcheckDisabled() && $this->custom_healthcheck_found === false) {
$this->newVersionIsHealthy = true;
return;
}
@ -808,7 +812,7 @@ private function health_check()
break;
}
$counter++;
sleep($this->application->health_check_interval);
Sleep::for($this->application->health_check_interval)->seconds();
}
}
}
@ -873,8 +877,8 @@ private function prepare_builder_image()
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
],
);
$this->run_pre_deployment_command();
}
private function deploy_to_additional_destinations()
{
@ -1077,7 +1081,10 @@ private function generate_env_variables()
private function generate_compose_file()
{
$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();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports);
@ -1088,6 +1095,25 @@ private function generate_compose_file()
$labels = $labels->filter(function ($value, $key) {
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->save();
} else {
@ -1097,6 +1123,18 @@ private function generate_compose_file()
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
$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 = [
'version' => '3.8',
'services' => [
@ -1109,16 +1147,6 @@ private function generate_compose_file()
'networks' => [
$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,
'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness,
@ -1135,6 +1163,18 @@ private function generate_compose_file()
]
]
];
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)) {
data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset);
}
@ -1235,24 +1275,6 @@ private function generate_compose_file()
if ($this->pull_request_id === 0) {
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];
data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
@ -1272,6 +1294,24 @@ private function generate_compose_file()
}
$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 @@ private function add_build_env_variables_to_dockerfile()
]);
}
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)
{
queue_next_deployment($this->application);
// 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([
'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));
return;
}

View File

@ -21,7 +21,8 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
public function __construct(
public Activity $activity,
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 @@ public function handle(): void
$remote_process = resolve(RunRemoteProcess::class, [
'activity' => $this->activity,
'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();

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 @@
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component;
class Index extends Component

View File

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

View File

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

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Profile;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate;
use Livewire\Component;
@ -10,6 +11,13 @@ class Index extends Component
public int $userId;
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')]
public string $name;
public function mount()
@ -19,7 +27,6 @@ public function mount()
$this->email = auth()->user()->email;
}
public function submit()
{
try {
$this->validate();
@ -27,7 +34,30 @@ public function submit()
'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) {
return handleError($e, $this);
}

View File

@ -66,6 +66,10 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'nullable',
'application.custom_labels' => '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_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
@ -112,6 +116,10 @@ public function mount()
} catch (\Throwable $e) {
$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->ports_exposes = $this->application->ports_exposes;
@ -120,7 +128,7 @@ public function mount()
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$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->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
@ -163,7 +171,12 @@ public function generateDomain(string $serviceName)
}
return $domain;
}
public function updatedApplicationBaseDirectory() {
raY('asdf');
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@ -211,12 +224,11 @@ public function updatedApplicationFqdn()
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
$this->resetDefaultLabels(false);
// $this->dispatch('success', 'Labels reset to default!');
}
public function submit($showToaster = true)
{
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->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();

View File

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

View File

@ -34,7 +34,7 @@ public function deleteBackup($exeuctionId)
}
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->dispatch('refreshBackupExecutions');
$this->refreshBackupExecutions();
}
public function download($exeuctionId)
{
@ -65,6 +65,6 @@ public function download($exeuctionId)
}
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 function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
@ -40,12 +39,17 @@ public function mount()
}
public function submit()
{
$server_id = $this->query['server_id'];
try {
$this->validate([
'dockerComposeRaw' => 'required'
]);
$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();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
@ -74,7 +78,6 @@ public function submit()
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

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

View File

@ -41,7 +41,6 @@ public function mount()
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
@ -55,6 +54,10 @@ public function submit()
{
try {
$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->saveExtraFields($this->fields);
$this->service->parse();

View File

@ -35,7 +35,7 @@ public function submit()
if (!$this->resource->limits_memory_swap) {
$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";
}
if (!$this->resource->limits_memory_reservation) {
@ -47,7 +47,7 @@ public function submit()
if ($this->resource->limits_cpuset === "") {
$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->validate();

View File

@ -45,7 +45,7 @@ public function cloneTo($destination_id)
'destination_id' => $new_destination->id,
]);
$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");
$new_resource->custom_labels = base64_encode($customLabels);
$new_resource->save();

View File

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

View File

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

View File

@ -14,9 +14,17 @@ class DynamicConfigurationNavbar extends Component
public function delete(string $fileName)
{
$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);
if ($proxy_type === 'CADDY' && $file === "Caddyfile") {
$this->dispatch('error', 'Cannot delete Caddyfile.');
return;
}
instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server);
if ($proxy_type === 'CADDY') {
$server->reloadCaddy();
}
$this->dispatch('success', 'File deleted.');
$this->dispatch('loadDynamicConfigurations');
$this->dispatch('refresh');

View File

@ -11,26 +11,32 @@ class DynamicConfigurations extends Component
public ?Server $server = null;
public $parameters = [];
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 = [
'contents.*' => 'nullable|string',
];
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 = collect(explode("\n", $files))->filter(fn ($file) => !empty($file));
$files = $files->map(fn ($file) => trim($file));
$files = $files->sort();
if ($files->contains('coolify.yaml')) {
$files = $files->filter(fn ($file) => $file !== 'coolify.yaml')->prepend('coolify.yaml');
}
$contents = collect([]);
foreach ($files as $file) {
$without_extension = str_replace('.', '|', $file);
$contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
}
$this->contents = $contents;
$this->dispatch('refresh');
}
public function mount()
{

View File

@ -29,7 +29,6 @@ public function addDynamicConfiguration()
'fileName' => 'required',
'value' => 'required',
]);
if (data_get($this->parameters, 'server_uuid')) {
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
}
@ -39,6 +38,8 @@ public function addDynamicConfiguration()
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$proxy_type = $this->server->proxyType();
if ($proxy_type === 'TRAEFIK_V2') {
if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) {
$this->fileName = "{$this->fileName}.yaml";
}
@ -46,7 +47,12 @@ public function addDynamicConfiguration()
$this->dispatch('error', 'File name is reserved.');
return;
}
$proxy_path = get_proxy_path();
} else if ($proxy_type === 'CADDY') {
if (!str($this->fileName)->endsWith('.caddy')) {
$this->fileName = "{$this->fileName}.caddy";
}
}
$proxy_path = $this->server->proxyPath();
$file = "{$proxy_path}/dynamic/{$this->fileName}";
if ($this->newFile) {
$exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server);
@ -55,11 +61,18 @@ public function addDynamicConfiguration()
return;
}
}
if ($proxy_type === 'TRAEFIK_V2') {
$yaml = Yaml::parse($this->value);
$yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml;
}
$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('dynamic-configuration-added');
$this->dispatch('success', 'Dynamic configuration saved.');

View File

@ -2,12 +2,9 @@
namespace App\Livewire\Settings;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server;
use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class Configuration extends Component
{
@ -78,13 +75,7 @@ public function submit()
$this->settings->save();
$this->server = Server::findOrFail(0);
$this->setup_instance_fqdn();
$this->server->setupDynamicProxyConfiguration();
$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 bool $is_system_wide;
public $applications;
protected $rules = [
'github_app.name' => 'required|string',
'github_app.organization' => 'nullable|string',
@ -90,6 +92,7 @@ public function mount()
if (!$this->github_app) {
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
$settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
@ -170,6 +173,11 @@ public function instantSave()
public function delete()
{
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();
return redirect()->route('source.all');
} catch (\Throwable $e) {

View File

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

View File

@ -27,7 +27,8 @@ protected static function booted()
$project->settings()->delete();
});
}
public function environment_variables() {
public function environment_variables()
{
return $this->hasMany(SharedEnvironmentVariable::class);
}
public function environments()
@ -45,6 +46,10 @@ public function team()
return $this->belongsTo(Team::class);
}
public function services()
{
return $this->hasManyThrough(Service::class, Environment::class);
}
public function applications()
{
return $this->hasManyThrough(Application::class, Environment::class);
@ -70,7 +75,8 @@ public function mariadbs()
{
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();
}
}

View File

@ -3,7 +3,6 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
@ -15,6 +14,8 @@
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
@ -118,18 +119,304 @@ public function addInitialNetwork()
}
}
}
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()
{
$proxyType = $this->proxy->get('type');
if ($proxyType === ProxyTypes::NONE->value) {
return $proxyType;
}
if (is_null($proxyType)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value;
$this->save();
}
return $this->proxy->get('type');
// $proxyType = $this->proxy->get('type');
// if ($proxyType === ProxyTypes::NONE->value) {
// return $proxyType;
// }
// if (is_null($proxyType)) {
// $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
// $this->proxy->status = ProxyStatus::EXITED->value;
// $this->save();
// }
return data_get($this->proxy, 'type');
}
public function scopeWithProxy(): Builder
{

View File

@ -102,6 +102,29 @@ public function extraFields()
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
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'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();

View File

@ -48,13 +48,15 @@ public function getRecepients($notification)
}
return explode(',', $recipients);
}
static public function serverLimitReached() {
static public function serverLimitReached()
{
$serverLimit = Team::serverLimit();
$team = currentTeam();
$servers = $team->servers->count();
return $servers >= $serverLimit;
}
public function serverOverflow() {
public function serverOverflow()
{
if ($this->serverLimit() < $this->servers->count()) {
return true;
}
@ -170,4 +172,17 @@ public function trialEndedButSubscribed()
]);
}
}
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 = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
];
protected $casts = [
'email_verified_at' => 'datetime',

View File

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

View File

@ -5,3 +5,7 @@ function get_team_id_from_token()
$token = auth()->user()->currentAccessToken();
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
use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
@ -215,6 +216,45 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
}
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)
{
$labels = collect([]);
@ -395,7 +435,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} else {
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
// Add Traefik labels
$labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
@ -404,6 +444,16 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_gzip_enabled: $application->isGzipEnabled(),
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();
}
@ -506,3 +556,25 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
}
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 Spatie\Url\Url;
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)
{
if ($server->isSwarm()) {
@ -75,7 +70,9 @@ function connectProxyToNetworks(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()) {
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
@ -98,6 +95,7 @@ function generate_default_proxy_configuration(Server $server)
"external" => true,
];
});
if ($proxy_type === 'TRAEFIK_V2') {
$labels = [
"traefik.enable=true",
"traefik.http.routers.traefik.entrypoints=http",
@ -176,209 +174,47 @@ function generate_default_proxy_configuration(Server $server)
} 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 {
return null;
}
$config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $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,
?Model $model = null,
bool $ignore_errors = false,
$callEventOnFinish = null
$callEventOnFinish = null,
$callEventData = null
): Activity {
if (is_null($type)) {
$type = ActivityTypes::INLINE->value;
@ -50,6 +51,7 @@ function remote_process(
model: $model,
ignore_errors: $ignore_errors,
call_event_on_finish: $callEventOnFinish,
call_event_data: $callEventData,
),
])();
}

View File

@ -80,7 +80,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
return handleError($e);
}
}
function updateCompose($resource)
function updateCompose(ServiceApplication|ServiceDatabase $resource)
{
try {
$name = data_get($resource, 'name');
@ -90,6 +90,9 @@ function updateCompose($resource)
// Switch Image
$image = data_get($resource, '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(',')) {
// Update FQDN
@ -105,7 +108,6 @@ function updateCompose($resource)
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($resource->fqdn);
$url = $url->getHost();
ray($url);
if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://');
$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) {
return handleError($e);
}

View File

@ -1,5 +1,6 @@
<?php
use App\Jobs\ServerFilesFromServerJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable;
@ -615,7 +616,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$allServices = getServiceTemplates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
@ -630,7 +631,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
$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', []));
$servicePorts = collect(data_get($service, 'ports', []));
$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;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
@ -897,18 +913,25 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
$last = $key->afterLast('_');
if (is_numeric($last->value())) {
// SERVICE_FQDN_3001
$port = $last;
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
if ($value) {
$path = $value->value();
} else {
$path = null;
}
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
@ -939,6 +962,25 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'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");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// 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->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 {
$generatedValue = generateEnvValue($command, $resource);
@ -1056,6 +1114,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
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()) {
@ -1354,10 +1422,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
if ($value) {
$path = $value->value();
} else {
$path = null;
}
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
@ -1495,7 +1564,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
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 [
...allowedPathsForUnsubscribedAccounts(),
'boarding',
'onboarding',
'livewire/update'
];
}

View File

@ -3,11 +3,11 @@
return [
// @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
// 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
'environment' => config('app.env'),

View File

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

View File

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

View File

@ -150,7 +150,17 @@ class="{{ request()->is('subscription*') ? 'text-warning icon' : 'icon' }}"
</a>
</li>
@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())
<li title="Settings" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="/settings">
@ -167,14 +177,14 @@ class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}"
</a>
</li>
@endif
<li title="Boarding" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('boarding') }}">
<svg class="{{ request()->is('boarding*') ? 'text-warning icon' : 'icon' }}"
<li title="Onboarding" class="hover:bg-coolgray-200">
<a class="hover:bg-transparent hover:no-underline" href="{{ route('onboarding') }}">
<svg class="{{ request()->is('onboarding*') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<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" />
</svg>
Boarding
Onboarding
</a>
</li>
</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) }}">
<button>Configuration</button>
</a>
@if (data_get($server, 'proxy.type') !== 'NONE')
@if ($server->proxyType() !== 'NONE')
{{-- @if ($server->proxyType() === 'TRAEFIK_V2') --}}
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'text-white' : '' }}"
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
<button>Dynamic Configurations</button>
</a>
{{-- @endif --}}
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
href="{{ route('server.proxy.logs', $parameters) }}">
<button>Logs</button>

View File

@ -8,6 +8,6 @@
{{ str($status)->before(':')->headline() }}
</div>
@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
</div>

View File

@ -17,17 +17,14 @@ class="text-warning">{{ session('currentTeam.name') }}</span></span>
<a class="{{ request()->routeIs('team.index') ? 'text-white' : '' }}" href="{{ route('team.index') }}">
<button>General</button>
</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>
</a>
<a class="{{ request()->routeIs('team.storage.index') ? 'text-white' : '' }}"
href="{{ route('team.storage.index') }}">
<button>S3 Storages</button>
</a>
<a class="{{ request()->routeIs('team.notification.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') }}">
<button>Shared Variables</button>

View File

@ -27,6 +27,7 @@
<ul x-data="{
toasts: [],
toastsHovered: false,
timeout: null,
expanded: false,
layout: 'default',
position: 'top-center',
@ -286,14 +287,12 @@
}
stackToasts();
$watch('toastsHovered', function(value) {
if (layout == 'default') {
if (position.includes('bottom')) {
resetBottom();
} else {
resetTop();
}
if (value) {
// calculate the new positions
expanded = true;
@ -319,13 +318,32 @@ class="fixed block w-full group z-[99] sm:max-w-xs"
<template x-for="(toast, index) in toasts" :key="toast.id">
<li :id="toast.id" x-data="{
toastHovered: false
toastHovered: false,
}" x-init="if (position.includes('bottom')) {
$el.firstElementChild.classList.add('toast-bottom');
$el.firstElementChild.classList.add('opacity-0', 'translate-y-full');
} else {
$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() {
@ -342,7 +360,7 @@ class="fixed block w-full group z-[99] sm:max-w-xs"
}, 5);
}, 50);
setTimeout(function() {
this.timeout = setTimeout(function() {
setTimeout(function() {
$el.firstElementChild.classList.remove('opacity-100');
$el.firstElementChild.classList.add('opacity-0');
@ -390,12 +408,12 @@ class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.
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>
</svg>
<p class="leading-2 text-neutral-200"
x-html="toast.message">
<p class="leading-2 text-neutral-200" x-html="toast.message">
</p>
</div>
<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>
</template>
<template x-if="toast.html">

View File

@ -3,7 +3,7 @@
<div>
@if ($currentState === 'welcome')
<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 ">
<x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get
Started
@ -24,12 +24,12 @@
<x-highlighted text="Self-hosting with superpowers!" /></span>
</x-slot:question>
<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>
<p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your server, so
everything works without Coolify (except integrations and automations).</p>
<p><x-highlighted text="Monitoring:" />You will get notified on your favourite platform (Discord,
Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
<p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your servers, so
everything works without a connection to Coolify (except integrations and automations).</p>
<p><x-highlighted text="Monitoring:" />You can get notified on your favourite platforms (Discord,
Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.</p>
</x-slot:explanation>
<x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:click="explanation">Next
@ -40,8 +40,8 @@
@if ($currentState === 'select-server-type')
<x-boarding-step title="Server">
<x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" />
or on a <x-highlighted text="Remote Server" />?
Do you want to deploy your resources to your <x-highlighted text="Localhost" />
or to a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<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?
@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
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Let's create a new
one!</x-forms.button>
<x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Create new
project!</x-forms.button>
<div>
@if (count($projects) > 0)
<form wire:submit='selectExistingProject' class="flex flex-col w-full gap-4 lg:w-96">
@ -319,9 +319,9 @@
</div>
</x-slot:actions>
<x-slot:explanation>
<p>Projects are bound together several resources into one virtual group. There are no
limitations on the number of projects you could have.</p>
<p>Each project should have at least one environment. This helps you to create a production &
<p>Projects contain several resources combined into one virtual group. There are no
limitations on the number of projects you can add.</p>
<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>
</x-slot:explanation>
</x-boarding-step>
@ -331,7 +331,7 @@
@if ($currentState === 'create-resource')
<x-boarding-step title="Resources">
<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:actions>
<div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do

View File

@ -17,7 +17,7 @@
@endif
@if ($projects->count() === 0 && $servers->count() === 0)
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
@if ($projects->count() > 0)
<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>
</a>
</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'>
<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 />

View File

@ -7,7 +7,8 @@ class="underline text-warning">Please
consider donating!</a>💜</span>
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and
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>
@endif
@ -20,4 +21,16 @@ class="text-white underline">/subscription</a> to update your subscription or re
</div>
</x-banner>
@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>

View File

@ -6,13 +6,22 @@
<h2>General</h2>
<x-forms.button type="submit" label="Save">Save</x-forms.button>
</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="email" label="Email" readonly />
</div>
</form>
<h2 class="py-4">Subscription</h2>
<a href="{{ route('team.index') }}">Check in Team Settings</a>
<form wire:submit='resetPassword' class="flex flex-col max-w-xl pt-4">
<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>
@if (session('status') == 'two-factor-authentication-enabled')
<div class="mb-4 font-medium">

View File

@ -250,6 +250,21 @@ class="underline" href="https://coolify.io/docs/docker/registry"
<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>
@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>
</form>
</div>

View File

@ -13,7 +13,10 @@
</x-modal>
<div class="pt-6">
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<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" />
</div>
</div>

View File

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

View File

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

View File

@ -16,7 +16,7 @@
</div>
<div class="w-96">
<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>
@if ($fields)
<div>

View File

@ -17,7 +17,7 @@ class="font-normal text-white normal-case border-none rounded btn btn-primary bt
</div>
<div>Environment variables (secrets) for this resource.</div>
@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
</div>
@if ($view === 'normal')

View File

@ -1,16 +1,29 @@
<div>
@if (data_get($server, 'proxy.type'))
@if ($server->proxyType())
<div x-init="$wire.loadProxyConfiguration">
@if ($selectedProxy === 'TRAEFIK_V2')
@if ($selectedProxy !== 'NONE')
<form wire:submit='submit'>
<div class="flex items-center gap-2">
<h2>Configuration</h2>
<x-forms.button type="submit">Save</x-forms.button>
@if ($server->proxy->status === 'exited')
<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
<x-forms.button type="submit">Save</x-forms.button>
</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 (
$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">
@if ($proxy_settings)
<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" />
<x-forms.button wire:click.prevent="reset_proxy_configuration">
Reset configuration to default
@ -40,7 +53,7 @@
<h2>Configuration</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div>
<div class="pt-3 pb-4">Custom (None) Proxy Selected</div>
<div class="pt-2 pb-4">Custom (None) Proxy Selected</div>
@else
<div class="flex items-center gap-2">
<h2>Configuration</h2>
@ -57,14 +70,13 @@
</x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('TRAEFIK_V2')">
Traefik
v2
</x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('CADDY')">
Caddy (experimental)
</x-forms.button>
<x-forms.button disabled class="box">
Nginx
</x-forms.button>
<x-forms.button disabled class="box">
Caddy
</x-forms.button>
</div>
</div>
@endif

View File

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

View File

@ -19,7 +19,7 @@ class="font-normal text-white normal-case border-none rounded btn btn-primary bt
Add</button>
</x-slide-over>
</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 wire:loading wire:target="loadDynamicConfigurations">
@ -29,12 +29,12 @@ class="font-normal text-white normal-case border-none rounded btn btn-primary bt
@if ($contents?->isNotEmpty())
@foreach ($contents as $fileName => $value)
<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>
<h3 class="text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
</div>
<x-forms.textarea disabled name="proxy_settings"
wire:model="contents.{{ $fileName }}" rows="10" />
wire:model="contents.{{ $fileName }}" rows="5" />
@else
<livewire:server.proxy.dynamic-configuration-navbar :server_id="$server->id"
:fileName="$fileName" :value="$value" :newFile="false"

View File

@ -1,5 +1,5 @@
<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.button type="submit" @click="slideOverOpen=false">Save</x-forms.button>
</form>

View File

@ -45,7 +45,8 @@
{{ 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>
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>
@ -138,6 +139,4 @@
</div>
</div>
</div>
</div>

View File

@ -69,7 +69,7 @@
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div>
<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>
<a href="{{ get_permissions_path($github_app) }}">
<x-forms.button>
@ -93,6 +93,57 @@
</div>
@endif
</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
<div class="flex items-center gap-2 pb-4">
<h1>GitHub App</h1>

View File

@ -1,5 +1,5 @@
<div>
<x-team.navbar />
<x-notifications.navbar />
<h2 class="pb-4">Notifications</h2>
<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">

View File

@ -1,8 +1,10 @@
<?php
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\Team;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
@ -29,11 +31,24 @@
return response(config('version'));
});
Route::get('/deploy', [Deploy::class, 'deploy']);
Route::get('/deployments', [Deploy::class, 'deployments']);
Route::get('/servers', [Server::class, 'servers']);
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('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']);
Route::get('/resources', [Resources::class, 'resources']);
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 () {

View File

@ -85,7 +85,7 @@
Route::get('/admin', AdminIndex::class)->name('admin.index');
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('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email');
Route::get('/email/verify/{id}/{hash}', [Controller::class, 'email_verify'])->middleware(['auth'])->name('verify.verify');
@ -108,7 +108,7 @@
});
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/new', SubscriptionIndex::class)->name('subscription.index');
@ -121,11 +121,13 @@
Route::get('/', TagsIndex::class)->name('tags.index');
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::get('/', TeamIndex::class)->name('team.index');
Route::get('/new', TeamCreate::class)->name('team.create');
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('/storages', TeamStorageIndex::class)->name('team.storage.index');
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 -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"
CDN="https://cdn.coollabs.io/coolify"
@ -122,6 +122,16 @@ if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################"
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 [ "$OS_TYPE" == 'almalinux' ]; then
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.
# tags: web, alert, monitor
# logo: svgs/changedetection.png
# port: 5000
services:
changedetection:
@ -9,7 +10,7 @@ services:
volumes:
- changedetection-data:/datastore
environment:
- SERVICE_FQDN_CHANGEDETECTION
- SERVICE_FQDN_CHANGEDETECTION_5000
- PUID=1000
- PGID=1000
- 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.
# tags: code, editor, remote, collaboration
# logo: svgs/code-server.svg
# port: 8443
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
environment:
- SERVICE_FQDN_CODESERVER
- SERVICE_FQDN_CODESERVER_8443
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid

View File

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

View File

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

View File

@ -2,12 +2,13 @@
# slogan: The Docker Registry is lets you distribute Docker images.
# tags: registry,images,docker
# logo: svgs/docker-registry.png
# port: 5000
services:
registry:
image: registry:2
environment:
- SERVICE_FQDN_REGISTRY
- SERVICE_FQDN_REGISTRY_5000
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry
- 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.
# tags: backup, encryption
# logo: svgs/duplicati.webp
# port: 8200
services:
duplicati:
image: lscr.io/linuxserver/duplicati:latest
environment:
- SERVICE_FQDN_DUPLICATI
- SERVICE_FQDN_DUPLICATI_8200
- PUID=1000
- PGID=1000
- 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.
# tags: media, server, movies, tv, music
# logo: svgs/emby.png
# port: 8096
services:
emby:
image: lscr.io/linuxserver/emby:latest
environment:
- SERVICE_FQDN_EMBY
- SERVICE_FQDN_EMBY_8096
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid

View File

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

View File

@ -2,12 +2,13 @@
# slogan: Fider is a feedback platform for collecting and managing user feedback.
# tags: feedback, user-feedback
# logo: svgs/fider.svg
# port: 3000
services:
fider:
image: getfider/fider:stable
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
JWT_SECRET: $SERVICE_PASSWORD_64_FIDER
EMAIL_NOREPLY: ${EMAIL_NOREPLY:-noreply@example.com}

View File

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

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