Merge branch 'next' into fix-migration

This commit is contained in:
🏔️ Peak
2024-12-10 18:33:25 +01:00
committed by GitHub
155 changed files with 3577 additions and 1817 deletions

View File

@@ -47,7 +47,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: | tags: |
@@ -82,7 +82,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: | tags: |

View File

@@ -42,7 +42,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: | tags: |
@@ -75,7 +75,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: | tags: |

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted; use App\Events\ProxyStarted;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'", "echo 'Successfully started coolify-proxy.'",
]); ]);
} else { } else {
$caddfile = 'import /dynamic/*.caddy'; if (isDev()) {
if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([ $commands = $commands->merge([
"mkdir -p $proxy_path/dynamic", "mkdir -p $proxy_path/dynamic",
"cd $proxy_path", "cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile", "echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'", "echo 'Pulling docker image.'",
'docker compose pull', 'docker compose pull',

View File

@@ -76,7 +76,5 @@ class Dev extends Command
} else { } else {
echo "Instance already initialized.\n"; echo "Instance already initialized.\n";
} }
// Set permissions
Process::run(['chmod', '-R', 'o+rwx', '.']);
} }
} }

View File

@@ -13,7 +13,7 @@ class Horizon extends Command
public function handle() public function handle()
{ {
if (config('constants.horizon.is_horizon_enabled')) { if (config('constants.horizon.is_horizon_enabled')) {
$this->info('[x]: Horizon is enabled. Starting.'); $this->info('Horizon is enabled on this server.');
$this->call('horizon'); $this->call('horizon');
exit(0); exit(0);
} else { } else {

View File

@@ -55,10 +55,8 @@ class Init extends Command
} else { } else {
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
} }
echo "[3]: Cleanup Redis keys.\n";
$this->call('cleanup:redis'); $this->call('cleanup:redis');
echo "[4]: Cleanup stucked resources.\n";
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
try { try {
@@ -114,7 +112,6 @@ class Init extends Command
private function optimize() private function optimize()
{ {
echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
Artisan::call('optimize:clear'); Artisan::call('optimize:clear');
Artisan::call('optimize'); Artisan::call('optimize');
} }
@@ -189,7 +186,6 @@ class Init extends Command
} }
} }
if ($commands->isNotEmpty()) { if ($commands->isNotEmpty()) {
echo "Cleaning up unused networks from coolify proxy\n";
remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false); remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -232,15 +228,14 @@ class Init extends Command
$settings = instanceSettings(); $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track'); $do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) { if ($do_not_track == true) {
echo "[2]: Skipping sending live signal as do_not_track is enabled\n"; echo "Do_not_track is enabled\n";
return; return;
} }
try { try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version"); Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "[2]: Error in sending live signal: {$e->getMessage()}\n"; echo "Error in sending live signal: {$e->getMessage()}\n";
} }
} }
@@ -253,7 +248,6 @@ class Init extends Command
} }
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
foreach ($queued_inprogress_deployments as $deployment) { foreach ($queued_inprogress_deployments as $deployment) {
echo "Cleaning up deployment: {$deployment->id}\n";
$deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save(); $deployment->save();
} }

View File

@@ -13,7 +13,7 @@ class Scheduler extends Command
public function handle() public function handle()
{ {
if (config('constants.horizon.is_scheduler_enabled')) { if (config('constants.horizon.is_scheduler_enabled')) {
$this->info('[x]: Scheduler is enabled. Starting.'); $this->info('Scheduler is enabled on this server.');
$this->call('schedule:work'); $this->call('schedule:work');
exit(0); exit(0);
} else { } else {

View File

@@ -25,26 +25,24 @@ class ApplicationsController extends Controller
{ {
private function removeSensitiveData($application) private function removeSensitiveData($application)
{ {
$token = auth()->user()->currentAccessToken();
$application->makeHidden([ $application->makeHidden([
'id', 'id',
]); ]);
if ($token->can('view:sensitive')) { if (request()->attributes->get('can_read_sensitive', false) === false) {
return serializeApiResponse($application); $application->makeHidden([
'custom_labels',
'dockerfile',
'docker_compose',
'docker_compose_raw',
'manual_webhook_secret_bitbucket',
'manual_webhook_secret_gitea',
'manual_webhook_secret_github',
'manual_webhook_secret_gitlab',
'private_key_id',
'value',
'real_value',
]);
} }
$application->makeHidden([
'custom_labels',
'dockerfile',
'docker_compose',
'docker_compose_raw',
'manual_webhook_secret_bitbucket',
'manual_webhook_secret_gitea',
'manual_webhook_secret_github',
'manual_webhook_secret_gitlab',
'private_key_id',
'value',
'real_value',
]);
return serializeApiResponse($application); return serializeApiResponse($application);
} }

View File

@@ -19,26 +19,23 @@ class DatabasesController extends Controller
{ {
private function removeSensitiveData($database) private function removeSensitiveData($database)
{ {
$token = auth()->user()->currentAccessToken();
$database->makeHidden([ $database->makeHidden([
'id', 'id',
'laravel_through_key', 'laravel_through_key',
]); ]);
if ($token->can('view:sensitive')) { if (request()->attributes->get('can_read_sensitive', false) === false) {
return serializeApiResponse($database); $database->makeHidden([
'internal_db_url',
'external_db_url',
'postgres_password',
'dragonfly_password',
'redis_password',
'mongo_initdb_root_password',
'keydb_password',
'clickhouse_admin_password',
]);
} }
$database->makeHidden([
'internal_db_url',
'external_db_url',
'postgres_password',
'dragonfly_password',
'redis_password',
'mongo_initdb_root_password',
'keydb_password',
'clickhouse_admin_password',
]);
return serializeApiResponse($database); return serializeApiResponse($database);
} }
@@ -211,8 +208,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +239,7 @@ class DatabasesController extends Controller
)] )]
public function update_by_uuid(Request $request) public function update_by_uuid(Request $request)
{ {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
@@ -413,12 +411,12 @@ class DatabasesController extends Controller
} }
break; break;
case 'standalone-mongodb': case 'standalone-mongodb':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mongo_conf' => 'string', 'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string', 'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string', 'mongo_initdb_database' => 'string',
]); ]);
if ($request->has('mongo_conf')) { if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) { if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +441,10 @@ class DatabasesController extends Controller
break; break;
case 'standalone-mysql': case 'standalone-mysql':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string', 'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string', 'mysql_user' => 'string',
'mysql_database' => 'string', 'mysql_database' => 'string',
'mysql_conf' => 'string', 'mysql_conf' => 'string',
@@ -909,6 +908,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1013,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type) public function create_database(Request $request, NewDatabaseTypes $type)
{ {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -1220,9 +1220,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) { } elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string', 'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string', 'mysql_user' => 'string',
'mysql_database' => 'string', 'mysql_database' => 'string',
'mysql_conf' => 'string', 'mysql_conf' => 'string',
@@ -1456,12 +1457,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) { } elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mongo_conf' => 'string', 'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string', 'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string', 'mongo_initdb_database' => 'string',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) { if ($validator->fails() || ! empty($extraFields)) {

View File

@@ -16,15 +16,12 @@ class DeployController extends Controller
{ {
private function removeSensitiveData($deployment) private function removeSensitiveData($deployment)
{ {
$token = auth()->user()->currentAccessToken(); if (request()->attributes->get('can_read_sensitive', false) === false) {
if ($token->can('view:sensitive')) { $deployment->makeHidden([
return serializeApiResponse($deployment); 'logs',
]);
} }
$deployment->makeHidden([
'logs',
]);
return serializeApiResponse($deployment); return serializeApiResponse($deployment);
} }

View File

@@ -11,13 +11,11 @@ class SecurityController extends Controller
{ {
private function removeSensitiveData($team) private function removeSensitiveData($team)
{ {
$token = auth()->user()->currentAccessToken(); if (request()->attributes->get('can_read_sensitive', false) === false) {
if ($token->can('view:sensitive')) { $team->makeHidden([
return serializeApiResponse($team); 'private_key',
]);
} }
$team->makeHidden([
'private_key',
]);
return serializeApiResponse($team); return serializeApiResponse($team);
} }

View File

@@ -19,25 +19,22 @@ class ServersController extends Controller
{ {
private function removeSensitiveDataFromSettings($settings) private function removeSensitiveDataFromSettings($settings)
{ {
$token = auth()->user()->currentAccessToken(); if (request()->attributes->get('can_read_sensitive', false) === false) {
if ($token->can('view:sensitive')) { $settings = $settings->makeHidden([
return serializeApiResponse($settings); 'sentinel_token',
]);
} }
$settings = $settings->makeHidden([
'sentinel_token',
]);
return serializeApiResponse($settings); return serializeApiResponse($settings);
} }
private function removeSensitiveData($server) private function removeSensitiveData($server)
{ {
$token = auth()->user()->currentAccessToken();
$server->makeHidden([ $server->makeHidden([
'id', 'id',
]); ]);
if ($token->can('view:sensitive')) { if (request()->attributes->get('can_read_sensitive', false) === false) {
return serializeApiResponse($server); // Do nothing
} }
return serializeApiResponse($server); return serializeApiResponse($server);

View File

@@ -18,19 +18,16 @@ class ServicesController extends Controller
{ {
private function removeSensitiveData($service) private function removeSensitiveData($service)
{ {
$token = auth()->user()->currentAccessToken();
$service->makeHidden([ $service->makeHidden([
'id', 'id',
]); ]);
if ($token->can('view:sensitive')) { if (request()->attributes->get('can_read_sensitive', false) === false) {
return serializeApiResponse($service); $service->makeHidden([
'docker_compose_raw',
'docker_compose',
]);
} }
$service->makeHidden([
'docker_compose_raw',
'docker_compose',
]);
return serializeApiResponse($service); return serializeApiResponse($service);
} }

View File

@@ -10,20 +10,18 @@ class TeamController extends Controller
{ {
private function removeSensitiveData($team) private function removeSensitiveData($team)
{ {
$token = auth()->user()->currentAccessToken();
$team->makeHidden([ $team->makeHidden([
'custom_server_limit', 'custom_server_limit',
'pivot', 'pivot',
]); ]);
if ($token->can('view:sensitive')) { if (request()->attributes->get('can_read_sensitive', false) === false) {
return serializeApiResponse($team); $team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
} }
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
return serializeApiResponse($team); return serializeApiResponse($team);
} }

View File

@@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem'); $private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret'); $webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([ $private_key = PrivateKey::create([
'name' => $slug, 'name' => "github-app-{$slug}",
'private_key' => $private_key, 'private_key' => $private_key,
'team_id' => $github_app->team_id, 'team_id' => $github_app->team_id,
'is_git_related' => true, 'is_git_related' => true,

View File

@@ -69,5 +69,7 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
'api.ability' => \App\Http\Middleware\ApiAbility::class,
'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class,
]; ];
} }

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
class ApiAbility extends CheckForAnyAbility
{
public function handle($request, $next, ...$abilities)
{
try {
if ($request->user()->tokenCan('root')) {
return $next($request);
}
return parent::handle($request, $next, ...$abilities);
} catch (\Illuminate\Auth\AuthenticationException $e) {
return response()->json([
'message' => 'Unauthenticated.',
], 401);
} catch (\Exception $e) {
return response()->json([
'message' => 'Missing required permissions: '.implode(', ', $abilities),
], 403);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiSensitiveData
{
public function handle(Request $request, Closure $next)
{
$token = $request->user()->currentAccessToken();
// Allow access to sensitive data if token has root or read:sensitive permission
$request->attributes->add([
'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
]);
return $next($request);
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IgnoreReadOnlyApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
if ($token->can('read-only')) {
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
return $next($request);
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OnlyRootApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
}

View File

@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null; private ?string $buildTarget = null;
private bool $disableBuildCache = false;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null; private ?string $full_healthcheck_url = null;
@@ -178,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id; $this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit; $this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback; $this->rollback = $this->application_deployment_queue->rollback;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild; $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
if ($this->disableBuildCache) {
$this->force_rebuild = true;
}
$this->restart_only = $this->application_deployment_queue->restart_only; $this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile'; $this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server; $this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -1976,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' '); $this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------'); $this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->disableBuildCache) {
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
}
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else { } else {

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToSlackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private SlackMessage $message,
private string $webhookUrl
) {
$this->onQueue('high');
}
public function handle(): void
{
Http::post($this->webhookUrl, [
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'plain_text',
'text' => 'Coolify Notification',
],
],
],
'attachments' => [
[
'color' => $this->message->color,
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => $this->message->title,
],
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $this->message->description,
],
],
],
],
],
]);
}
}

View File

@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void public function handle(ProxyStarted $event): void
{ {
$this->server = data_get($event, 'data'); $this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect(); $this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration(); $this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false; $this->server->proxy->force_stop = false;
$this->server->save(); $this->server->save();

View File

@@ -37,7 +37,7 @@ class Email extends Component
#[Validate(['nullable', 'numeric'])] #[Validate(['nullable', 'numeric'])]
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]

View File

@@ -0,0 +1,131 @@
<?php
namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Slack extends Component
{
public Team $team;
#[Validate(['boolean'])]
public bool $slackEnabled = false;
#[Validate(['url', 'nullable'])]
public ?string $slackWebhookUrl = null;
#[Validate(['boolean'])]
public bool $slackNotificationsTest = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDeployments = false;
#[Validate(['boolean'])]
public bool $slackNotificationsStatusChanges = false;
#[Validate(['boolean'])]
public bool $slackNotificationsDatabaseBackups = false;
#[Validate(['boolean'])]
public bool $slackNotificationsScheduledTasks = false;
#[Validate(['boolean'])]
public bool $slackNotificationsServerDiskUsage = false;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->team->slack_enabled = $this->slackEnabled;
$this->team->slack_webhook_url = $this->slackWebhookUrl;
$this->team->slack_notifications_test = $this->slackNotificationsTest;
$this->team->slack_notifications_deployments = $this->slackNotificationsDeployments;
$this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges;
$this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups;
$this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks;
$this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage;
$this->team->save();
refreshSession();
} else {
$this->slackEnabled = $this->team->slack_enabled;
$this->slackWebhookUrl = $this->team->slack_webhook_url;
$this->slackNotificationsTest = $this->team->slack_notifications_test;
$this->slackNotificationsDeployments = $this->team->slack_notifications_deployments;
$this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes;
$this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups;
$this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks;
$this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage;
}
}
public function instantSaveSlackEnabled()
{
try {
$this->validate([
'slackWebhookUrl' => 'required',
], [
'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->slackEnabled = false;
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
try {
$this->team->notify(new Test);
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.notifications.slack');
}
}

View File

@@ -25,6 +25,9 @@ class Advanced extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true; public bool $isAutoDeployEnabled = true;
#[Validate(['boolean'])]
public bool $disableBuildCache = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
@@ -95,6 +98,7 @@ class Advanced extends Component
$this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
$this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
$this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
$this->application->settings->disable_build_cache = $this->disableBuildCache;
$this->application->settings->save(); $this->application->settings->save();
} else { } else {
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
@@ -116,6 +120,7 @@ class Advanced extends Component
$this->customInternalName = $this->application->settings->custom_internal_name; $this->customInternalName = $this->application->settings->custom_internal_name;
$this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
$this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
} }
} }

View File

@@ -24,6 +24,14 @@ class Executions extends Component
#[Locked] #[Locked]
public ?string $serverTimezone = null; public ?string $serverTimezone = null;
public $currentPage = 1;
public $logsPerPage = 100;
public $selectedExecution = null;
public $isPollingActive = false;
public function getListeners() public function getListeners()
{ {
$teamId = Auth::user()->currentTeam()->id; $teamId = Auth::user()->currentTeam()->id;
@@ -54,16 +62,84 @@ class Executions extends Component
public function refreshExecutions(): void public function refreshExecutions(): void
{ {
$this->executions = $this->task->executions()->take(20)->get(); $this->executions = $this->task->executions()->take(20)->get();
if ($this->selectedKey) {
$this->selectedExecution = $this->task->executions()->find($this->selectedKey);
if ($this->selectedExecution && $this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
} }
public function selectTask($key): void public function selectTask($key): void
{ {
if ($key == $this->selectedKey) { if ($key == $this->selectedKey) {
$this->selectedKey = null; $this->selectedKey = null;
$this->selectedExecution = null;
$this->currentPage = 1;
$this->isPollingActive = false;
return; return;
} }
$this->selectedKey = $key; $this->selectedKey = $key;
$this->selectedExecution = $this->task->executions()->find($key);
$this->currentPage = 1;
// Start polling if task is running
if ($this->selectedExecution && $this->selectedExecution->status === 'running') {
$this->isPollingActive = true;
}
}
public function polling()
{
if ($this->selectedExecution && $this->isPollingActive) {
$this->selectedExecution->refresh();
if ($this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
}
public function loadMoreLogs()
{
$this->currentPage++;
}
public function getLogLinesProperty()
{
if (! $this->selectedExecution) {
return collect();
}
if (! $this->selectedExecution->message) {
return collect(['Waiting for task output...']);
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->take($this->currentPage * $this->logsPerPage);
}
public function downloadLogs(int $executionId)
{
$execution = $this->executions->firstWhere('id', $executionId);
if (! $execution) {
return;
}
return response()->streamDownload(function () use ($execution) {
echo $execution->message;
}, 'task-execution-'.$execution->id.'.log');
}
public function hasMoreLogs()
{
if (! $this->selectedExecution || ! $this->selectedExecution->message) {
return false;
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->count() > ($this->currentPage * $this->logsPerPage);
} }
public function formatDateInServerTimezone($date) public function formatDateInServerTimezone($date)

View File

@@ -11,13 +11,7 @@ class ApiTokens extends Component
public $tokens = []; public $tokens = [];
public bool $viewSensitiveData = false; public array $permissions = ['read'];
public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only'];
public $isApiEnabled; public $isApiEnabled;
@@ -29,51 +23,28 @@ class ApiTokens extends Component
public function mount() public function mount()
{ {
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled; $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->getTokens();
}
private function getTokens()
{
$this->tokens = auth()->user()->tokens->sortByDesc('created_at'); $this->tokens = auth()->user()->tokens->sortByDesc('created_at');
} }
public function updatedViewSensitiveData() public function updatedPermissions($permissionToUpdate)
{ {
if ($this->viewSensitiveData) { if ($permissionToUpdate == 'root') {
$this->permissions[] = 'view:sensitive'; $this->permissions = ['root'];
$this->permissions = array_diff($this->permissions, ['*']); } elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
$this->rootAccess = false; $this->permissions[] = 'read';
} elseif ($permissionToUpdate == 'deploy') {
$this->permissions = ['deploy'];
} else { } else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']); if (count($this->permissions) == 0) {
} $this->permissions = ['read'];
$this->makeSureOneIsSelected(); }
}
public function updatedReadOnly()
{
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
$this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
} }
sort($this->permissions);
} }
public function addNewToken() public function addNewToken()
@@ -82,8 +53,8 @@ class ApiTokens extends Component
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
]); ]);
$token = auth()->user()->createToken($this->description, $this->permissions); $token = auth()->user()->createToken($this->description, array_values($this->permissions));
$this->tokens = auth()->user()->tokens; $this->getTokens();
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -92,8 +63,12 @@ class ApiTokens extends Component
public function revoke(int $id) public function revoke(int $id)
{ {
$token = auth()->user()->tokens()->where('id', $id)->first(); try {
$token->delete(); $token = auth()->user()->tokens()->where('id', $id)->firstOrFail();
$this->tokens = auth()->user()->tokens; $token->delete();
$this->getTokens();
} catch (\Exception $e) {
return handleError($e, $this);
}
} }
} }

View File

@@ -15,6 +15,8 @@ class Proxy extends Component
public $proxy_settings = null; public $proxy_settings = null;
public bool $redirect_enabled = true;
public ?string $redirect_url = null; public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
@@ -26,6 +28,7 @@ class Proxy extends Component
public function mount() public function mount()
{ {
$this->selectedProxy = $this->server->proxyType(); $this->selectedProxy = $this->server->proxyType();
$this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url'); $this->redirect_url = data_get($this->server, 'proxy.redirect_url');
} }
@@ -38,7 +41,7 @@ class Proxy extends Component
{ {
$this->server->proxy = null; $this->server->proxy = null;
$this->server->save(); $this->server->save();
$this->dispatch('proxyChanged'); $this->dispatch('reloadWindow');
} }
public function selectProxy($proxy_type) public function selectProxy($proxy_type)
@@ -46,7 +49,7 @@ class Proxy extends Component
try { try {
$this->server->changeProxy($proxy_type, async: false); $this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
$this->dispatch('proxyStatusUpdated'); $this->dispatch('reloadWindow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -63,13 +66,25 @@ class Proxy extends Component
} }
} }
public function instantSaveRedirect()
{
try {
$this->server->proxy->redirect_enabled = $this->redirect_enabled;
$this->server->save();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {
SaveConfiguration::run($this->server, $this->proxy_settings); SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url; $this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save(); $this->server->save();
$this->server->setupDefault404Redirect(); $this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.'); $this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -65,7 +65,7 @@ class Deploy extends Component
public function restart() public function restart()
{ {
try { try {
$this->stop(forceStop: false); $this->stop();
$this->dispatch('checkProxy'); $this->dispatch('checkProxy');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -105,6 +105,7 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp(); $startTime = Carbon::now()->getTimestamp();
while ($process->running()) { while ($process->running()) {
ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName); $this->forceStopContainer($containerName);
break; break;

View File

@@ -19,7 +19,7 @@ class SettingsEmail extends Component
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]

View File

@@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob; use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\PrivateKey;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Livewire\Component; use Livewire\Component;
class Change extends Component class Change extends Component
@@ -51,12 +56,20 @@ class Change extends Component
'github_app.administration' => 'nullable|string', 'github_app.administration' => 'nullable|string',
]; ];
public function boot()
{
if ($this->github_app) {
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
}
}
public function checkPermissions() public function checkPermissions()
{ {
GithubAppPermissionJob::dispatchSync($this->github_app); GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.'); $this->dispatch('success', 'Github App permissions updated.');
} }
// public function check() // public function check()
// { // {
@@ -90,15 +103,16 @@ class Change extends Component
// ray($runners_by_repository); // ray($runners_by_repository);
// } // }
public function mount() public function mount()
{ {
try { try {
$github_app_uuid = request()->github_app_uuid; $github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
$this->applications = $this->github_app->applications; $this->applications = $this->github_app->applications;
$settings = instanceSettings(); $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab(); $this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn; $this->fqdn = $settings->fqdn;
@@ -142,6 +156,77 @@ class Change extends Component
} }
} }
public function getGithubAppNameUpdatePath()
{
if (str($this->github_app->organization)->isNotEmpty()) {
return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}";
}
return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}";
}
private function generateGithubJwt($private_key, $app_id): string
{
$configuration = Configuration::forAsymmetricSigner(
new Sha256,
InMemory::plainText($private_key),
InMemory::plainText($private_key)
);
$now = time();
return $configuration->builder()
->issuedBy((string) $app_id)
->permittedFor('https://api.github.com')
->identifiedBy((string) $now)
->issuedAt(new \DateTimeImmutable("@{$now}"))
->expiresAt(new \DateTimeImmutable('@'.($now + 600)))
->getToken($configuration->signer(), $configuration->signingKey())
->toString();
}
public function updateGithubAppName()
{
try {
$privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id);
if (! $privateKey) {
$this->dispatch('error', 'No private key found for this GitHub App.');
return;
}
$jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id);
$response = Http::withHeaders([
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
'Authorization' => "Bearer {$jwt}",
])->get("{$this->github_app->api_url}/app");
if ($response->successful()) {
$app_data = $response->json();
$app_slug = $app_data['slug'] ?? null;
if ($app_slug) {
$this->github_app->name = $app_slug;
$this->name = str($app_slug)->kebab();
$privateKey->name = "github-app-{$app_slug}";
$privateKey->save();
$this->github_app->save();
$this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.');
} else {
$this->dispatch('info', 'Could not find App Name (slug) in GitHub response.');
}
} else {
$error_message = $response->json()['message'] ?? 'Unknown error';
$this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}");
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -1321,17 +1321,43 @@ class Application extends BaseModel
if (! $gitRemoteStatus['is_accessible']) { if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
} }
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
$commands = collect([ if (version_compare($gitVersion, '2.35.1', '<')) {
"rm -rf /tmp/{$uuid}", $fileList = $fileList->map(function ($file) {
"mkdir -p /tmp/{$uuid}", $parts = explode('/', trim($file, '.'));
"cd /tmp/{$uuid}", $paths = collect();
$cloneCommand, $currentPath = '';
'git sparse-checkout init --cone', foreach ($parts as $part) {
"git sparse-checkout set {$fileList->implode(' ')}", $currentPath .= ($currentPath ? '/' : '').$part;
'git read-tree -mu HEAD', $paths->push($currentPath);
"cat .$workdir$composeFile", }
]);
return $paths;
})->flatten()->unique()->values();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
} else {
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
}
try { try {
$composeFileContent = instant_remote_process($commands, $this->destination->server); $composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -20,7 +20,7 @@ abstract class BaseModel extends Model
}); });
} }
public function name(): Attribute public function sanitizedName(): Attribute
{ {
return new Attribute( return new Attribute(
get: fn () => sanitize_string($this->getRawOriginal('name')), get: fn () => sanitize_string($this->getRawOriginal('name')),

View File

@@ -105,6 +105,14 @@ class Server extends BaseModel
]); ]);
} }
} }
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::retrieved(function ($server) {
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
}); });
static::forceDeleting(function ($server) { static::forceDeleting(function ($server) {
@@ -184,73 +192,80 @@ class Server extends BaseModel
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
} }
public function setupDefault404Redirect() public function setupDefaultRedirect()
{ {
$banner =
"# This file is generated by Coolify, do not edit it manually.\n".
"# Disable the default redirect to customize (only if you know what are you doing).\n\n";
$dynamic_conf_path = $this->proxyPath().'/dynamic'; $dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType(); $proxy_type = $this->proxyType();
$redirect_enabled = $this->proxy->redirect_enabled ?? true;
$redirect_url = $this->proxy->redirect_url; $redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) { if (isDev()) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if ($proxy_type === ProxyTypes::CADDY->value) { if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 { $dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
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 | tee $default_redirect_file > /dev/null",
], $this);
$this->reloadCaddy();
return;
} }
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $default_redirect_file",
], $this);
return;
} }
if ($proxy_type === ProxyTypes::TRAEFIK->value) { if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [ $default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
'http' => [ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
'routers' => [ $default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy";
'catchall' => [ }
'entryPoints' => [
0 => 'http', instant_remote_process([
1 => 'https', "mkdir -p $dynamic_conf_path",
], "rm -f $dynamic_conf_path/default_redirect_404.yaml",
'service' => 'noop', "rm -f $dynamic_conf_path/default_redirect_404.caddy",
'rule' => 'HostRegexp(`.+`)', ], $this);
'tls' => [
'certResolver' => 'letsencrypt', if ($redirect_enabled === false) {
], instant_remote_process(["rm -f $default_redirect_file"], $this);
'priority' => 1, } else {
'middlewares' => [ if ($proxy_type === ProxyTypes::CADDY->value) {
0 => 'redirect-regexp', if (filled($redirect_url)) {
$conf = ":80, :443 {
redir $redirect_url
}";
} else {
$conf = ':80, :443 {
respond 503
}';
}
} elseif ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'PathPrefix(`/`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => -1000,
], ],
], ],
], 'services' => [
'services' => [ 'noop' => [
'noop' => [ 'loadBalancer' => [
'loadBalancer' => [ 'servers' => [],
'servers' => [
0 => [
'url' => '',
],
], ],
], ],
], ],
], ],
'middlewares' => [ ];
if (filled($redirect_url)) {
$dynamic_conf['http']['routers']['catchall']['middlewares'] = [
0 => 'redirect-regexp',
];
$dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [
'url' => '',
];
$dynamic_conf['http']['middlewares'] = [
'redirect-regexp' => [ 'redirect-regexp' => [
'redirectRegex' => [ 'redirectRegex' => [
'regex' => '(.*)', 'regex' => '(.*)',
@@ -258,32 +273,17 @@ respond 404
'permanent' => false, 'permanent' => false,
], ],
], ],
], ];
], }
]; $conf = Yaml::dump($dynamic_conf, 12, 2);
$conf = Yaml::dump($dynamic_conf, 12, 2); }
$conf = $conf = $banner.$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);
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$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); $base64 = base64_encode($conf);
instant_remote_process([
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
} }
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
if ($proxy_type === 'CADDY') { if ($proxy_type === 'CADDY') {
$this->reloadCaddy(); $this->reloadCaddy();
} }
@@ -611,7 +611,9 @@ $schema://$host {
} }
$memory = json_decode($memory, true); $memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) { $parsedCollection = collect($memory)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['usedPercent']]; $usedPercent = $metric['usedPercent'] ?? 0.0;
return [(int) $metric['time'], (float) $usedPercent];
}); });
return $parsedCollection->toArray(); return $parsedCollection->toArray();

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsSlack;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -70,7 +71,7 @@ use OpenApi\Attributes as OA;
), ),
] ]
)] )]
class Team extends Model implements SendsDiscord, SendsEmail class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{ {
use Notifiable; use Notifiable;
@@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail
]; ];
} }
public function routeNotificationForSlack()
{
return data_get($this, 'slack_webhook_url', null);
}
public function getRecepients($notification) public function getRecepients($notification)
{ {
$recipients = data_get($notification, 'emails', null); $recipients = data_get($notification, 'emails', null);

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class DeploymentFailed extends CustomEmailNotification class DeploymentFailed extends CustomEmailNotification
@@ -128,4 +129,31 @@ class DeploymentFailed extends CustomEmailNotification
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} deployment failed";
$description = "Pull request deployment failed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = 'Deployment failed';
$description = "Deployment failed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class DeploymentSuccess extends CustomEmailNotification class DeploymentSuccess extends CustomEmailNotification
@@ -143,4 +144,31 @@ class DeploymentSuccess extends CustomEmailNotification
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
if ($this->preview) {
$title = "Pull request #{$this->preview->pull_request_id} successfully deployed";
$description = "New version successfully deployed for {$this->application_name}";
if ($this->preview->fqdn) {
$description .= "\nPreview URL: {$this->preview->fqdn}";
}
} else {
$title = 'New version successfully deployed';
$description = "New version successfully deployed for {$this->application_name}";
if ($this->fqdn) {
$description .= "\nApplication URL: {$this->fqdn}";
}
}
$description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Deployment Logs:** {$this->deployment_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Application;
use App\Models\Application; use App\Models\Application;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class StatusChanged extends CustomEmailNotification class StatusChanged extends CustomEmailNotification
@@ -75,4 +76,20 @@ class StatusChanged extends CustomEmailNotification
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
$title = 'Application stopped';
$description = "{$this->resource_name} has been stopped";
$description .= "\n\n**Project:** ".data_get($this->resource, 'environment.project.name');
$description .= "\n**Environment:** {$this->environment_name}";
$description .= "\n**Application URL:** {$this->resource_url}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -66,11 +66,12 @@ class EmailChannel
'transport' => 'smtp', 'transport' => 'smtp',
'host' => data_get($notifiable, 'smtp_host'), 'host' => data_get($notifiable, 'smtp_host'),
'port' => data_get($notifiable, 'smtp_port'), 'port' => data_get($notifiable, 'smtp_port'),
'encryption' => data_get($notifiable, 'smtp_encryption'), 'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'),
'username' => data_get($notifiable, 'smtp_username'), 'username' => data_get($notifiable, 'smtp_username'),
'password' => data_get($notifiable, 'smtp_password'), 'password' => data_get($notifiable, 'smtp_password'),
'timeout' => data_get($notifiable, 'smtp_timeout'), 'timeout' => data_get($notifiable, 'smtp_timeout'),
'local_domain' => null, 'local_domain' => null,
'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '',
]); ]);
} }
} }

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Notifications\Channels;
interface SendsSlack
{
public function routeNotificationForSlack();
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToSlackJob;
use Illuminate\Notifications\Notification;
class SlackChannel
{
/**
* Send the given notification.
*/
public function send(SendsSlack $notifiable, Notification $notification): void
{
$message = $notification->toSlack();
$webhookUrl = $notifiable->routeNotificationForSlack();
if (! $webhookUrl) {
return;
}
SendMessageToSlackJob::dispatch($message, $webhookUrl);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Container;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ContainerRestarted extends CustomEmailNotification class ContainerRestarted extends CustomEmailNotification
@@ -66,4 +67,20 @@ class ContainerRestarted extends CustomEmailNotification
return $payload; return $payload;
} }
public function toSlack(): SlackMessage
{
$title = 'Resource restarted';
$description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::warningColor()
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Container;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ContainerStopped extends CustomEmailNotification class ContainerStopped extends CustomEmailNotification
@@ -66,4 +67,20 @@ class ContainerStopped extends CustomEmailNotification
return $payload; return $payload;
} }
public function toSlack(): SlackMessage
{
$title = 'Resource stopped';
$description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}";
if ($this->url) {
$description .= "\n**Resource URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class BackupFailed extends CustomEmailNotification class BackupFailed extends CustomEmailNotification
@@ -62,4 +63,19 @@ class BackupFailed extends CustomEmailNotification
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = 'Database backup failed';
$description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
$description .= "\n\n**Frequency:** {$this->frequency}";
$description .= "\n\n**Error Output:**\n{$this->output}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class BackupSuccess extends CustomEmailNotification class BackupSuccess extends CustomEmailNotification
@@ -60,4 +61,18 @@ class BackupSuccess extends CustomEmailNotification
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = 'Database backup successful';
$description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
$description .= "\n\n**Frequency:** {$this->frequency}";
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::successColor()
);
}
} }

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Notifications\Dto;
class SlackMessage
{
public function __construct(
public string $title,
public string $description,
public string $color = '#0099ff'
) {}
public static function infoColor(): string
{
return '#0099ff';
}
public static function errorColor(): string
{
return '#ff0000';
}
public static function successColor(): string
{
return '#00ff00';
}
public static function warningColor(): string
{
return '#ffa500';
}
}

View File

@@ -3,8 +3,10 @@
namespace App\Notifications\Internal; namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
@@ -25,6 +27,7 @@ class GeneralNotification extends Notification implements ShouldQueue
$channels = []; $channels = [];
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -32,6 +35,9 @@ class GeneralNotification extends Notification implements ShouldQueue
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -51,4 +57,13 @@ class GeneralNotification extends Notification implements ShouldQueue
'message' => $this->message, 'message' => $this->message,
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Coolify: General Notification',
description: $this->message,
color: SlackMessage::infoColor(),
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class TaskFailed extends CustomEmailNotification class TaskFailed extends CustomEmailNotification
@@ -68,4 +69,24 @@ class TaskFailed extends CustomEmailNotification
'message' => $message, 'message' => $message,
]; ];
} }
public function toSlack(): SlackMessage
{
$title = 'Scheduled task failed';
$description = "Scheduled task ({$this->task->name}) failed.";
if ($this->output) {
$description .= "\n\n**Error Output:**\n{$this->output}";
}
if ($this->url) {
$description .= "\n\n**Task URL:** {$this->url}";
}
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -7,6 +7,7 @@ use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
class DockerCleanup extends CustomEmailNotification class DockerCleanup extends CustomEmailNotification
{ {
@@ -21,7 +22,7 @@ class DockerCleanup extends CustomEmailNotification
// $isEmailEnabled = isEmailEnabled($notifiable); // $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -31,6 +32,9 @@ class DockerCleanup extends CustomEmailNotification
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -62,4 +66,13 @@ class DockerCleanup extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", 'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server cleanup job done',
description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ForceDisabled extends CustomEmailNotification class ForceDisabled extends CustomEmailNotification
@@ -23,7 +25,7 @@ class ForceDisabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -33,6 +35,9 @@ class ForceDisabled extends CustomEmailNotification
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -67,4 +72,18 @@ class ForceDisabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
]; ];
} }
public function toSlack(): SlackMessage
{
$title = 'Server disabled';
$description = "Server ({$this->server->name}) disabled because it is not paid!\n";
$description .= "All automations and integrations are stopped.\n\n";
$description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscriptions';
return new SlackMessage(
title: $title,
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ForceEnabled extends CustomEmailNotification class ForceEnabled extends CustomEmailNotification
@@ -23,7 +25,7 @@ class ForceEnabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -33,6 +35,9 @@ class ForceEnabled extends CustomEmailNotification
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -63,4 +68,13 @@ class ForceEnabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) enabled again!", 'message' => "Coolify: Server ({$this->server->name}) enabled again!",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server enabled',
description: "Server '{$this->server->name}' enabled again!",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class HighDiskUsage extends CustomEmailNotification class HighDiskUsage extends CustomEmailNotification
@@ -55,4 +56,22 @@ class HighDiskUsage extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
]; ];
} }
public function toSlack(): SlackMessage
{
$description = "Server '{$this->server->name}' high disk usage detected!\n";
$description .= "Disk usage: {$this->disk_usage}%\n";
$description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n";
$description .= "Please cleanup your disk to prevent data-loss.\n";
$description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n";
$description .= "Change settings:\n";
$description .= '- Threshold: '.base_url().'/server/'.$this->server->uuid."#advanced\n";
$description .= '- Notifications: '.base_url().'/notifications/discord';
return new SlackMessage(
title: 'High disk usage detected',
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class Reachable extends CustomEmailNotification class Reachable extends CustomEmailNotification
@@ -32,7 +34,7 @@ class Reachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
@@ -42,6 +44,9 @@ class Reachable extends CustomEmailNotification
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -72,4 +77,13 @@ class Reachable extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!", 'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!",
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Server revived',
description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!",
color: SlackMessage::successColor()
);
}
} }

View File

@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification; use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class Unreachable extends CustomEmailNotification class Unreachable extends CustomEmailNotification
@@ -32,6 +34,7 @@ class Unreachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) { if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -42,6 +45,9 @@ class Unreachable extends CustomEmailNotification
if ($isTelegramEnabled) { if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }
@@ -76,4 +82,17 @@ class Unreachable extends CustomEmailNotification
'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.",
]; ];
} }
public function toSlack(): SlackMessage
{
$description = "Your server '{$this->server->name}' is unreachable.\n";
$description .= "All automations & integrations are turned off!\n\n";
$description .= '*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations.';
return new SlackMessage(
title: 'Server unreachable',
description: $description,
color: SlackMessage::errorColor()
);
}
} }

View File

@@ -3,6 +3,7 @@
namespace App\Notifications; namespace App\Notifications;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -67,4 +68,12 @@ class Test extends Notification implements ShouldQueue
], ],
]; ];
} }
public function toSlack(): SlackMessage
{
return new SlackMessage(
title: 'Test Slack Notification',
description: 'This is a test Slack notification from Coolify.'
);
}
} }

View File

@@ -15,6 +15,7 @@ class Checkbox extends Component
public ?string $id = null, public ?string $id = null,
public ?string $name = null, public ?string $name = null,
public ?string $value = null, public ?string $value = null,
public ?string $domValue = null,
public ?string $label = null, public ?string $label = null,
public ?string $helper = null, public ?string $helper = null,
public string|bool|null $checked = false, public string|bool|null $checked = false,

View File

@@ -288,9 +288,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$host_without_www = str($host)->replace('www.', ''); $host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme(); $schema = $url->getScheme();
$port = $url->getPort(); $port = $url->getPort();
$handle = "handle_path"; $handle = 'handle_path';
if ( ! $is_stripprefix_enabled){ if (! $is_stripprefix_enabled) {
$handle = "handle"; $handle = 'handle';
} }
if (is_null($port) && ! is_null($onlyPort)) { if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort; $port = $onlyPort;
@@ -302,7 +302,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_{$loop}.header=-Server"); $labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
if ($port) { if ($port) {
$labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}"); $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}");
} else { } else {

View File

@@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server)
], ],
'volumes' => [ 'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro', '/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}:/traefik",
], ],
'command' => [ 'command' => [
'--ping=true', '--ping=true',
'--ping.entrypoint=http', '--ping.entrypoint=http',
'--api.dashboard=true', '--api.dashboard=true',
'--api.insecure=false',
'--entrypoints.http.address=:80', '--entrypoints.http.address=:80',
'--entrypoints.https.address=:443', '--entrypoints.https.address=:443',
'--entrypoints.http.http.encodequerysemicolons=true', '--entrypoints.http.http.encodequerysemicolons=true',
@@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server)
'--entrypoints.https.http.encodequerysemicolons=true', '--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50', '--entryPoints.https.http2.maxConcurrentStreams=50',
'--entrypoints.https.http3', '--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/', '--providers.file.directory=/traefik/dynamic/',
'--providers.docker.exposedbydefault=false',
'--providers.file.watch=true', '--providers.file.watch=true',
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true', '--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
], ],
'labels' => $labels, 'labels' => $labels,
], ],
], ],
]; ];
if (isDev()) { if (isDev()) {
// $config['services']['traefik']['command'][] = "--log.level=debug"; $config['services']['traefik']['command'][] = '--api.insecure=true';
$config['services']['traefik']['command'][] = '--log.level=debug';
$config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log'; $config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log';
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100'; $config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
$config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik';
} else {
$config['services']['traefik']['command'][] = '--api.insecure=false';
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
} }
if ($server->isSwarm()) { if ($server->isSwarm()) {
data_forget($config, 'services.traefik.container_name'); data_forget($config, 'services.traefik.container_name');

View File

@@ -27,6 +27,7 @@ use App\Models\Team;
use App\Models\User; use App\Models\User;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification; use App\Notifications\Internal\GeneralNotification;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
@@ -469,11 +470,13 @@ function setNotificationChannels($notifiable, $event)
{ {
$channels = []; $channels = [];
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isSlackEnabled = data_get($notifiable, 'slack_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event"); $isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
$isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
@@ -484,6 +487,9 @@ function setNotificationChannels($notifiable, $event)
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class; $channels[] = TelegramChannel::class;
} }
if ($isSlackEnabled && $isSubscribedToSlackEvent) {
$channels[] = SlackChannel::class;
}
return $channels; return $channels;
} }

View File

@@ -13,7 +13,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"3sidedcube/laravel-redoc": "^1.0", "3sidedcube/laravel-redoc": "^1.0",
"danharrin/livewire-rate-limiting": "^1.1", "danharrin/livewire-rate-limiting": "2.0.0",
"doctrine/dbal": "^4.2", "doctrine/dbal": "^4.2",
"guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/guzzle": "^7.5.0",
"laravel/fortify": "^1.16.0", "laravel/fortify": "^1.16.0",

1488
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.376', 'version' => '4.0.0-beta.377',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'), 'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View File

@@ -0,0 +1,60 @@
<?php
use App\Models\PersonalAccessToken;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
try {
$tokens = PersonalAccessToken::all();
foreach ($tokens as $token) {
$abilities = collect();
if (in_array('*', $token->abilities)) {
$abilities->push('root');
}
if (in_array('read-only', $token->abilities)) {
$abilities->push('read');
}
if (in_array('view:sensitive', $token->abilities)) {
$abilities->push('read', 'read:sensitive');
}
$token->abilities = $abilities->unique()->values()->all();
$token->save();
}
} catch (\Exception $e) {
\Log::error('Error renaming token permissions: '.$e->getMessage());
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
try {
$tokens = PersonalAccessToken::all();
foreach ($tokens as $token) {
$abilities = collect();
if (in_array('write', $token->abilities)) {
$abilities->push('*');
} else {
if (in_array('read', $token->abilities)) {
$abilities->push('read-only');
}
if (in_array('read:sensitive', $token->abilities)) {
$abilities->push('view:sensitive');
}
}
$token->abilities = $abilities->unique()->values()->all();
$token->save();
}
} catch (\Exception $e) {
\Log::error('Error renaming token permissions: '.$e->getMessage());
}
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->boolean('slack_enabled')->default(false);
$table->string('slack_webhook_url')->nullable();
$table->boolean('slack_notifications_test')->default(true);
$table->boolean('slack_notifications_deployments')->default(true);
$table->boolean('slack_notifications_status_changes')->default(true);
$table->boolean('slack_notifications_database_backups')->default(true);
$table->boolean('slack_notifications_scheduled_tasks')->default(true);
$table->boolean('slack_notifications_server_disk_usage')->default(true);
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn([
'slack_enabled',
'slack_webhook_url',
'slack_notifications_test',
'slack_notifications_deployments',
'slack_notifications_status_changes',
'slack_notifications_database_backups',
'slack_notifications_scheduled_tasks',
'slack_notifications_server_disk_usage',
]);
});
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('disable_build_cache')->default(false);
});
}
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('disable_build_cache');
});
}
};

View File

@@ -23,6 +23,7 @@ class GithubAppSeeder extends Seeder
GithubApp::create([ GithubApp::create([
'name' => 'coolify-laravel-development-public', 'name' => 'coolify-laravel-development-public',
'uuid' => '69420', 'uuid' => '69420',
'organization' => 'coollabsio',
'api_url' => 'https://api.github.com', 'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com', 'html_url' => 'https://github.com',
'is_public' => false, 'is_public' => false,

View File

@@ -22,9 +22,9 @@ class ProductionSeeder extends Seeder
public function run(): void public function run(): void
{ {
if (isCloud()) { if (isCloud()) {
echo "[x]: Running in cloud mode.\n"; echo " Running in cloud mode.\n";
} else { } else {
echo "[x]: Running in self-hosted mode.\n"; echo " Running in self-hosted mode.\n";
} }
// Fix for 4.0.0-beta.37 // Fix for 4.0.0-beta.37

View File

@@ -2,15 +2,14 @@ services:
coolify: coolify:
build: build:
context: . context: .
dockerfile: ./docker/dev/Dockerfile dockerfile: ./docker/development/Dockerfile
args:
- USER_ID=${USERID:-1000}
- GROUP_ID=${GROUPID:-1000}
ports: ports:
- "${APP_PORT:-8000}:80" - "${APP_PORT:-8000}:80"
environment: environment:
PUID: "${USERID:-1000}" AUTORUN_ENABLED: false
PGID: "${GROUPID:-1000}"
SSL_MODE: "off"
AUTORUN_LARAVEL_STORAGE_LINK: "false"
AUTORUN_LARAVEL_MIGRATION: "false"
PUSHER_HOST: "${PUSHER_HOST}" PUSHER_HOST: "${PUSHER_HOST}"
PUSHER_PORT: "${PUSHER_PORT}" PUSHER_PORT: "${PUSHER_PORT}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}" PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"

View File

@@ -1,61 +0,0 @@
# Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.0
# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15
ARG POSTGRES_VERSION=15
FROM minio/mc:${MINIO_VERSION} AS minio-client
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION
ARG MINIO_VERSION
ARG POSTGRES_VERSION
# Use build arguments for caching
ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
# Install dependencies
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
apt-get install -y $BUILDTIME_DEPS && \
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
apt-get update && \
apt-get install -y $RUNTIME_DEPS && \
apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
RUN --mount=type=cache,target=/root/.cache \
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN --mount=type=cache,target=/root/.cache \
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc
RUN { \
echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini

View File

@@ -1,5 +0,0 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:horizon"
}

View File

@@ -1,5 +0,0 @@
#!/command/execlineb -P
foreground { composer -d /var/www/html/ install }
foreground { php /var/www/html/artisan migrate --step }
foreground { php /var/www/html/artisan dev --init }

View File

@@ -1,5 +0,0 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:scheduler"
}

View File

@@ -0,0 +1,81 @@
# Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-17T19-35-25Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.1
# https://www.postgresql.org/support/versioning/
ARG POSTGRES_VERSION=15
# =================================================================
# Get MinIO client
# =================================================================
FROM minio/mc:${MINIO_VERSION} AS minio-client
# =================================================================
# Final Stage: Production image
# =================================================================
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG USER_ID
ARG GROUP_ID
ARG TARGETPLATFORM
ARG POSTGRES_VERSION
ARG CLOUDFLARED_VERSION
WORKDIR /var/www/html
USER root
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
# Install PostgreSQL repository and keys
RUN apk add --no-cache gnupg && \
mkdir -p /usr/share/keyrings && \
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg
# Install system dependencies
RUN apk add --no-cache \
postgresql${POSTGRES_VERSION}-client \
openssh-client \
git \
git-lfs \
jq \
lsof \
vim
# Configure shell aliases
RUN echo "alias ll='ls -al'" >> /etc/profile && \
echo "alias a='php artisan'" >> /etc/profile && \
echo "alias logs='tail -f storage/logs/laravel.log'" >> /etc/profile
# Install Cloudflared based on architecture
RUN mkdir -p /usr/local/bin && \
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \
elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \
fi && \
chmod +x /usr/local/bin/cloudflared
# Configure PHP
COPY docker/development/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
ENV PHP_OPCACHE_ENABLE=0
# Configure Nginx and S6 overlay
COPY docker/development/etc/nginx/conf.d/custom.conf /etc/nginx/conf.d/custom.conf
COPY docker/development/etc/nginx/site-opts.d/http.conf /etc/nginx/site-opts.d/http.conf
COPY --chmod=755 docker/development/etc/s6-overlay/ /etc/s6-overlay/
RUN mkdir -p /etc/nginx/conf.d && \
chown -R www-data:www-data /etc/nginx && \
chmod -R 755 /etc/nginx
# Install MinIO client
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc
# Switch to non-root user
USER www-data

View File

@@ -0,0 +1,45 @@
listen 80 default_server;
listen [::]:80 default_server;
listen 8080 default_server;
listen [::]:8080 default_server;
root /var/www/html/public;
# Set allowed "index" files
index index.html index.htm index.php;
server_name _;
charset utf-8;
# Set max upload to 2048M
client_max_body_size 2048M;
# Healthchecks: Set /healthcheck to be the healthcheck URL
location /healthcheck {
access_log off;
# set max 5 seconds for healthcheck
fastcgi_read_timeout 5s;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /healthcheck;
fastcgi_param SCRIPT_FILENAME /healthcheck;
fastcgi_pass 127.0.0.1:9000;
}
# Have NGINX try searching for PHP files as well
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass "*.php" files to PHP-FPM
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffers 8 8k;
fastcgi_buffer_size 8k;
fastcgi_read_timeout 99;
}

View File

@@ -0,0 +1,9 @@
error_reporting = E_ERROR
error_log = /dev/stderr
log_errors = On
log_errors_max_len = 8192
ignore_repeated_errors = On
ignore_repeated_source = On
upload_max_filesize = 256M
post_max_size = 256M

View File

@@ -0,0 +1,12 @@
#!/command/execlineb -P
# Use with-contenv to ensure environment variables are available
with-contenv
cd /var/www/html
foreground {
php
artisan
start:horizon
}

View File

@@ -0,0 +1,22 @@
#!/command/execlineb -P
# Use with-contenv to ensure environment variables are available
with-contenv
cd /var/www/html
foreground {
composer
install
}
foreground {
php
artisan
migrate
--step
}
foreground {
php
artisan
dev
--init
}

View File

@@ -0,0 +1,13 @@
#!/command/execlineb -P
# Use with-contenv to ensure environment variables are available
with-contenv
cd /var/www/html
foreground {
php
artisan
start:scheduler
}

View File

@@ -1,82 +0,0 @@
# Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.0
# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15
ARG POSTGRES_VERSION=15
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} AS base
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
FROM node:20 AS static-assets
WORKDIR /app
COPY . .
COPY --from=base --chown=9999:9999 /var/www/html .
RUN npm install
RUN npm run build
FROM minio/mc:${MINIO_VERSION} AS minio-client
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION
ARG POSTGRES_VERSION
ARG CI=true
WORKDIR /var/www/html
RUN apt-get update
# Postgres version requirements
RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
RUN apt-get update
RUN apt-get install postgresql-client-${POSTGRES_VERSION} -y
# Coolify requirements
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof vim
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY docker/prod/nginx.conf /etc/nginx/conf.d/custom.conf
COPY --from=base --chown=9999:9999 /var/www/html .
COPY --chown=9999:9999 . .
RUN composer dump-autoload
COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build
COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN { \
echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc

View File

@@ -1,2 +0,0 @@
#!/command/execlineb -P
php /var/www/html/artisan migrate --force --isolated

View File

@@ -1,5 +0,0 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:horizon"
}

View File

@@ -1,3 +0,0 @@
#!/command/execlineb -P
s6-setuidgid webuser
php /var/www/html/artisan app:init

View File

@@ -1,2 +0,0 @@
#!/command/execlineb -P
php /var/www/html/artisan db:seed --class ProductionSeeder --force

View File

@@ -1,5 +0,0 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:scheduler"
}

View File

@@ -0,0 +1,136 @@
# Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-17T19-35-25Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.1
# https://www.postgresql.org/support/versioning/
ARG POSTGRES_VERSION=15
# Add user/group
ARG USER_ID=9999
ARG GROUP_ID=9999
# =================================================================
# Stage 1: Composer dependencies
# =================================================================
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} AS base
USER root
ARG USER_ID
ARG GROUP_ID
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
WORKDIR /var/www/html
COPY --chown=www-data:www-data composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
USER www-data
# =================================================================
# Stage 2: Frontend assets compilation
# =================================================================
FROM node:20-alpine AS static-assets
WORKDIR /app
COPY package*.json vite.config.js tailwind.config.js postcss.config.cjs ./
RUN npm ci
COPY . .
RUN npm run build
# =================================================================
# Stage 3: Get MinIO client
# =================================================================
FROM minio/mc:${MINIO_VERSION} AS minio-client
# =================================================================
# Final Stage: Production image
# =================================================================
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG USER_ID
ARG GROUP_ID
ARG TARGETPLATFORM
ARG POSTGRES_VERSION
ARG CLOUDFLARED_VERSION
ARG CI=true
WORKDIR /var/www/html
USER root
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
# Install PostgreSQL repository and keys
RUN apk add --no-cache gnupg && \
mkdir -p /usr/share/keyrings && \
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg
# Install system dependencies
RUN apk add --no-cache \
postgresql${POSTGRES_VERSION}-client \
openssh-client \
git \
git-lfs \
jq \
lsof \
vim
# Configure shell aliases
RUN echo "alias ll='ls -al'" >> /etc/profile && \
echo "alias a='php artisan'" >> /etc/profile && \
echo "alias logs='tail -f storage/logs/laravel.log'" >> /etc/profile
# Install Cloudflared based on architecture
RUN mkdir -p /usr/local/bin && \
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \
elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \
fi && \
chmod +x /usr/local/bin/cloudflared
# Configure PHP
COPY docker/production/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
ENV PHP_OPCACHE_ENABLE=1
# Copy application files from previous stages
COPY --from=base --chown=www-data:www-data /var/www/html/vendor ./vendor
COPY --from=static-assets --chown=www-data:www-data /app/public/build ./public/build
# Copy application source code
COPY --chown=www-data:www-data composer.json composer.lock ./
COPY --chown=www-data:www-data app ./app
COPY --chown=www-data:www-data bootstrap ./bootstrap
COPY --chown=www-data:www-data config ./config
COPY --chown=www-data:www-data database ./database
COPY --chown=www-data:www-data lang ./lang
COPY --chown=www-data:www-data public ./public
COPY --chown=www-data:www-data routes ./routes
COPY --chown=www-data:www-data storage ./storage
COPY --chown=www-data:www-data templates ./templates
COPY --chown=www-data:www-data resources/views ./resources/views
COPY --chown=www-data:www-data artisan artisan
RUN composer dump-autoload
# Configure Nginx and S6 overlay
COPY docker/production/etc/nginx/conf.d/custom.conf /etc/nginx/conf.d/custom.conf
COPY docker/production/etc/nginx/site-opts.d/http.conf /etc/nginx/site-opts.d/http.conf
COPY --chmod=755 docker/production/etc/s6-overlay/ /etc/s6-overlay/
RUN mkdir -p /etc/nginx/conf.d && \
chown -R www-data:www-data /etc/nginx && \
chmod -R 755 /etc/nginx
# Install MinIO client
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc
# Switch to non-root user
USER www-data

View File

@@ -0,0 +1,45 @@
listen 80 default_server;
listen [::]:80 default_server;
listen 8080 default_server;
listen [::]:8080 default_server;
root /var/www/html/public;
# Set allowed "index" files
index index.html index.htm index.php;
server_name _;
charset utf-8;
# Set max upload to 2048M
client_max_body_size 2048M;
# Healthchecks: Set /healthcheck to be the healthcheck URL
location /healthcheck {
access_log off;
# set max 5 seconds for healthcheck
fastcgi_read_timeout 5s;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /healthcheck;
fastcgi_param SCRIPT_FILENAME /healthcheck;
fastcgi_pass 127.0.0.1:9000;
}
# Have NGINX try searching for PHP files as well
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass "*.php" files to PHP-FPM
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffers 8 8k;
fastcgi_buffer_size 8k;
fastcgi_read_timeout 99;
}

View File

@@ -0,0 +1,9 @@
error_reporting = E_ERROR
error_log = /var/www/html/storage/logs/php-error.log
log_errors = Off
log_errors_max_len = 8192
ignore_repeated_errors = On
ignore_repeated_source = On
upload_max_filesize = 256M
post_max_size = 256M

View File

@@ -0,0 +1,13 @@
#!/command/execlineb -P
# Use with-contenv to ensure environment variables are available
with-contenv
cd /var/www/html
foreground {
php
artisan
migrate
--force
--isolated
}

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