@@ -238,7 +238,7 @@ After completing these steps, you'll have a fresh development setup.
|
||||
### Contributing a New Service
|
||||
|
||||
To add a new service to Coolify, please refer to our documentation:
|
||||
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
|
||||
[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
|
||||
|
||||
### Contributing to Documentation
|
||||
|
||||
|
56
app/Actions/Proxy/StopProxy.php
Normal file
56
app/Actions/Proxy/StopProxy.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$timeout = 30;
|
||||
|
||||
$process = $this->stopContainer($containerName, $timeout);
|
||||
|
||||
$startTime = Carbon::now()->getTimestamp();
|
||||
while ($process->running()) {
|
||||
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName, $server);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName, $server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
} finally {
|
||||
$server->proxy->force_stop = $forceStop;
|
||||
$server->proxy->status = 'exited';
|
||||
$server->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName, Server $server)
|
||||
{
|
||||
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName, Server $server)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||
}
|
||||
}
|
@@ -2881,198 +2881,198 @@ class ApplicationsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Execute Command',
|
||||
description: "Execute a command on the application's current container.",
|
||||
path: '/applications/{uuid}/execute',
|
||||
operationId: 'execute-command-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Command to execute.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: "Execute a command on the application's current container.",
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
'response' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function execute_command_by_uuid(Request $request)
|
||||
{
|
||||
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
$allowedFields = ['command'];
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'command' => 'string|required',
|
||||
]);
|
||||
// #[OA\Post(
|
||||
// summary: 'Execute Command',
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// path: '/applications/{uuid}/execute',
|
||||
// operationId: 'execute-command-application',
|
||||
// security: [
|
||||
// ['bearerAuth' => []],
|
||||
// ],
|
||||
// tags: ['Applications'],
|
||||
// parameters: [
|
||||
// new OA\Parameter(
|
||||
// name: 'uuid',
|
||||
// in: 'path',
|
||||
// description: 'UUID of the application.',
|
||||
// required: true,
|
||||
// schema: new OA\Schema(
|
||||
// type: 'string',
|
||||
// format: 'uuid',
|
||||
// )
|
||||
// ),
|
||||
// ],
|
||||
// requestBody: new OA\RequestBody(
|
||||
// required: true,
|
||||
// description: 'Command to execute.',
|
||||
// content: new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// responses: [
|
||||
// new OA\Response(
|
||||
// response: 200,
|
||||
// description: "Execute a command on the application's current container.",
|
||||
// content: [
|
||||
// new OA\MediaType(
|
||||
// mediaType: 'application/json',
|
||||
// schema: new OA\Schema(
|
||||
// type: 'object',
|
||||
// properties: [
|
||||
// 'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||
// 'response' => ['type' => 'string'],
|
||||
// ]
|
||||
// )
|
||||
// ),
|
||||
// ]
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 401,
|
||||
// ref: '#/components/responses/401',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 400,
|
||||
// ref: '#/components/responses/400',
|
||||
// ),
|
||||
// new OA\Response(
|
||||
// response: 404,
|
||||
// ref: '#/components/responses/404',
|
||||
// ),
|
||||
// ]
|
||||
// )]
|
||||
// public function execute_command_by_uuid(Request $request)
|
||||
// {
|
||||
// // TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||
// $allowedFields = ['command'];
|
||||
// $teamId = getTeamIdFromToken();
|
||||
// if (is_null($teamId)) {
|
||||
// return invalidTokenResponse();
|
||||
// }
|
||||
// $uuid = $request->route('uuid');
|
||||
// if (! $uuid) {
|
||||
// return response()->json(['message' => 'UUID is required.'], 400);
|
||||
// }
|
||||
// $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
// if (! $application) {
|
||||
// return response()->json(['message' => 'Application not found.'], 404);
|
||||
// }
|
||||
// $return = validateIncomingRequest($request);
|
||||
// if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
// return $return;
|
||||
// }
|
||||
// $validator = customApiValidator($request->all(), [
|
||||
// 'command' => 'string|required',
|
||||
// ]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
// $extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
// if ($validator->fails() || ! empty($extraFields)) {
|
||||
// $errors = $validator->errors();
|
||||
// if (! empty($extraFields)) {
|
||||
// foreach ($extraFields as $field) {
|
||||
// $errors->add($field, 'This field is not allowed.');
|
||||
// }
|
||||
// }
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => $errors,
|
||||
// ], 422);
|
||||
// }
|
||||
|
||||
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
// $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||
// $status = getContainerStatus($application->destination->server, $container['Names']);
|
||||
|
||||
if ($status !== 'running') {
|
||||
return response()->json([
|
||||
'message' => 'Application is not running.',
|
||||
], 400);
|
||||
}
|
||||
// if ($status !== 'running') {
|
||||
// return response()->json([
|
||||
// 'message' => 'Application is not running.',
|
||||
// ], 400);
|
||||
// }
|
||||
|
||||
$commands = collect([
|
||||
executeInDocker($container['Names'], $request->command),
|
||||
]);
|
||||
// $commands = collect([
|
||||
// executeInDocker($container['Names'], $request->command),
|
||||
// ]);
|
||||
|
||||
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
// $res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Command executed.',
|
||||
'response' => $res,
|
||||
]);
|
||||
}
|
||||
// return response()->json([
|
||||
// 'message' => 'Command executed.',
|
||||
// 'response' => $res,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
// private function validateDataApplications(Request $request, Server $server)
|
||||
// {
|
||||
// $teamId = getTeamIdFromToken();
|
||||
|
||||
// Validate ports_mappings
|
||||
if ($request->has('ports_mappings')) {
|
||||
$ports = [];
|
||||
foreach (explode(',', $request->ports_mappings) as $portMapping) {
|
||||
$port = explode(':', $portMapping);
|
||||
if (in_array($port[0], $ports)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'ports_mappings' => 'The first number before : should be unique between mappings.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$ports[] = $port[0];
|
||||
}
|
||||
}
|
||||
// Validate custom_labels
|
||||
if ($request->has('custom_labels')) {
|
||||
if (! isBase64Encoded($request->custom_labels)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$customLabels = base64_decode($request->custom_labels);
|
||||
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
$uuid = $request->uuid;
|
||||
$fqdn = $request->domains;
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$errors = [];
|
||||
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
|
||||
$errors[] = 'Invalid domain: '.$domain;
|
||||
}
|
||||
// // Validate ports_mappings
|
||||
// if ($request->has('ports_mappings')) {
|
||||
// $ports = [];
|
||||
// foreach (explode(',', $request->ports_mappings) as $portMapping) {
|
||||
// $port = explode(':', $portMapping);
|
||||
// if (in_array($port[0], $ports)) {
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => [
|
||||
// 'ports_mappings' => 'The first number before : should be unique between mappings.',
|
||||
// ],
|
||||
// ], 422);
|
||||
// }
|
||||
// $ports[] = $port[0];
|
||||
// }
|
||||
// }
|
||||
// // Validate custom_labels
|
||||
// if ($request->has('custom_labels')) {
|
||||
// if (! isBase64Encoded($request->custom_labels)) {
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => [
|
||||
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
// ],
|
||||
// ], 422);
|
||||
// }
|
||||
// $customLabels = base64_decode($request->custom_labels);
|
||||
// if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => [
|
||||
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
|
||||
// ],
|
||||
// ], 422);
|
||||
// }
|
||||
// }
|
||||
// if ($request->has('domains') && $server->isProxyShouldRun()) {
|
||||
// $uuid = $request->uuid;
|
||||
// $fqdn = $request->domains;
|
||||
// $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
// $fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
// $errors = [];
|
||||
// $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
// if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
|
||||
// $errors[] = 'Invalid domain: '.$domain;
|
||||
// }
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
if (count($errors) > 0) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'domains' => 'One of the domain is already used.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
// return str($domain)->trim()->lower();
|
||||
// });
|
||||
// if (count($errors) > 0) {
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => $errors,
|
||||
// ], 422);
|
||||
// }
|
||||
// if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
|
||||
// return response()->json([
|
||||
// 'message' => 'Validation failed.',
|
||||
// 'errors' => [
|
||||
// 'domains' => 'One of the domain is already used.',
|
||||
// ],
|
||||
// ], 422);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@@ -329,13 +329,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$this->write_deployment_configurations();
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
|
||||
'hidden' => true,
|
||||
'ignore_errors' => true,
|
||||
]
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
|
||||
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
}
|
||||
@@ -1366,13 +1361,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
'command' => "docker rm -f {$this->deployment_uuid}",
|
||||
'ignore_errors' => true,
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
|
||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
$runCommand,
|
||||
|
46
app/Jobs/RestartProxyJob.php
Normal file
46
app/Jobs/RestartProxyJob.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server);
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
StartProxy::run($this->server, force: true);
|
||||
|
||||
CheckProxy::run($this->server, true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,11 +4,10 @@ namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
|
||||
class Deploy extends Component
|
||||
@@ -65,6 +64,7 @@ class Deploy extends Component
|
||||
public function restart()
|
||||
{
|
||||
try {
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
$this->dispatch('checkProxy');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -97,43 +97,10 @@ class Deploy extends Component
|
||||
public function stop(bool $forceStop = true)
|
||||
{
|
||||
try {
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$timeout = 30;
|
||||
|
||||
$process = $this->stopContainer($containerName, $timeout);
|
||||
|
||||
$startTime = Carbon::now()->getTimestamp();
|
||||
while ($process->running()) {
|
||||
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
|
||||
$this->forceStopContainer($containerName);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$this->removeContainer($containerName);
|
||||
StopProxy::run($this->server, $forceStop);
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->server->proxy->force_stop = $forceStop;
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
private function forceStopContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
|
||||
private function removeContainer(string $containerName)
|
||||
{
|
||||
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ class SettingsBackup extends Component
|
||||
{
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public ?StandalonePostgresql $database = null;
|
||||
|
||||
public ScheduledDatabaseBackup|null|array $backup = [];
|
||||
@@ -46,6 +48,7 @@ class SettingsBackup extends Component
|
||||
return redirect()->route('dashboard');
|
||||
} else {
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
|
||||
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
|
||||
if ($this->database) {
|
||||
@@ -60,6 +63,10 @@ class SettingsBackup extends Component
|
||||
$this->database->save();
|
||||
}
|
||||
$this->backup = $this->database->scheduledBackups->first();
|
||||
if ($this->backup && ! $this->server->isFunctional()) {
|
||||
$this->backup->enabled = false;
|
||||
$this->backup->save();
|
||||
}
|
||||
$this->executions = $this->backup->executions;
|
||||
}
|
||||
$this->settings = $settings;
|
||||
|
@@ -4,7 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Test;
|
||||
use App\Notifications\TransactionalEmails\Test;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
|
@@ -12,8 +12,9 @@ class EmailChannel
|
||||
public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
{
|
||||
$useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
|
||||
$isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
|
||||
$customEmails = data_get($notification, 'emails', null);
|
||||
if ($useInstanceEmailSettings) {
|
||||
if ($useInstanceEmailSettings || $isTransactionalEmail) {
|
||||
$settings = instanceSettings();
|
||||
} else {
|
||||
$settings = $notifiable->emailNotificationSettings;
|
||||
@@ -49,8 +50,8 @@ class EmailChannel
|
||||
$settings->smtp_port,
|
||||
$encryption
|
||||
);
|
||||
$transport->setUsername($settings->smtp_username);
|
||||
$transport->setPassword($settings->smtp_password);
|
||||
$transport->setUsername($settings->smtp_username ?? '');
|
||||
$transport->setPassword($settings->smtp_password ?? '');
|
||||
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
|
||||
|
@@ -16,9 +16,10 @@ class InvitationLink extends CustomEmailNotification
|
||||
return [TransactionalEmailChannel::class];
|
||||
}
|
||||
|
||||
public function __construct(public User $user)
|
||||
public function __construct(public User $user, public bool $isTransactionalEmail)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->isTransactionalEmail = true;
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
|
@@ -17,10 +17,11 @@ class ResetPassword extends Notification
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public function __construct($token)
|
||||
public function __construct($token, public bool $isTransactionalEmail)
|
||||
{
|
||||
$this->settings = instanceSettings();
|
||||
$this->token = $token;
|
||||
$this->isTransactionalEmail = true;
|
||||
}
|
||||
|
||||
public static function createUrlUsing($callback)
|
||||
|
@@ -8,9 +8,10 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class Test extends CustomEmailNotification
|
||||
{
|
||||
public function __construct(public string $emails)
|
||||
public function __construct(public string $emails, public string $isTransactionalEmail)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->isTransactionalEmail = true;
|
||||
}
|
||||
|
||||
public function via(): array
|
||||
|
40
lang/az.json
Normal file
40
lang/az.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"auth.login": "Daxil ol",
|
||||
"auth.login.authentik": "Authentik ilə daxil ol",
|
||||
"auth.login.azure": "Azure ilə daxil ol",
|
||||
"auth.login.bitbucket": "Bitbucket ilə daxil ol",
|
||||
"auth.login.github": "Github ilə daxil ol",
|
||||
"auth.login.gitlab": "GitLab ilə daxil ol",
|
||||
"auth.login.google": "Google ilə daxil ol",
|
||||
"auth.login.infomaniak": "Infomaniak ilə daxil ol",
|
||||
"auth.already_registered": "Qeytiyatınız var?",
|
||||
"auth.confirm_password": "Şifrəni təsdiqləyin",
|
||||
"auth.forgot_password": "Şifrəmi unutdum",
|
||||
"auth.forgot_password_send_email": "Şifrəni sıfırlamaq üçün e-poçt göndər",
|
||||
"auth.register_now": "Qeydiyyat",
|
||||
"auth.logout": "Çıxış",
|
||||
"auth.register": "Qeydiyyat",
|
||||
"auth.registration_disabled": "Qeydiyyat bağlıdır. Administratorla əlaqə saxlayın.",
|
||||
"auth.reset_password": "Şifrənin bərpası",
|
||||
"auth.failed": "Bu məlumatlar bizim qeydlərimizlə uyğun gəlmir.",
|
||||
"auth.failed.callback": "Giriş təminatçısından geri çağırma işlənə bilmədi.",
|
||||
"auth.failed.password": "Daxil etdiyiniz şifrə yanlışdır.",
|
||||
"auth.failed.email": "Bu e-poçt ünvanı ilə istifadəçi tapılmadı.",
|
||||
"auth.throttle": "Çox sayda uğursuz giriş cəhdi. Zəhmət olmasa :seconds saniyə sonra yenidən cəhd edin.",
|
||||
"input.name": "Ad",
|
||||
"input.email": "E-poçt",
|
||||
"input.password": "Şifrə",
|
||||
"input.password.again": "Şifrəni təkrar daxil edin",
|
||||
"input.code": "Bir dəfəlik kod",
|
||||
"input.recovery_code": "Bərpa kodu",
|
||||
"button.save": "Yadda saxla",
|
||||
"repository.url": "<span class='text-helper'>Nümunələr</span><br>Publik repozitoriyalar üçün <span class='text-helper'>https://...</span> istifadə edin.<br>Özəl repozitoriyalar üçün <span class='text-helper'>git@...</span> istifadə edin.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch-ı seçiləcək<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch-ı seçiləcək.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch-ı seçiləcək.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch-ı seçiləcək.",
|
||||
"service.stop": "Bu xidmət dayandırılacaq.",
|
||||
"resource.docker_cleanup": "Docker təmizlənməsini işə salın (istifadə olunmayan şəkillər və builder keşini silin).",
|
||||
"resource.non_persistent": "Bütün qeyri-daimi məlumatlar silinəcək.",
|
||||
"resource.delete_volumes": "Bu resursla əlaqəli bütün həcm məlumatları tamamilə silinəcək.",
|
||||
"resource.delete_connected_networks": "Bu resursla əlaqəli bütün əvvəlcədən təyin olunmamış şəbəkələr tamamilə silinəcək.",
|
||||
"resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.",
|
||||
"database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.",
|
||||
"warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https <span class='dark:text-red-500 text-red-500 font-bold'>TÖVSİYƏ EDİLMİR</span>, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq). <br><br>Əvəzində öz domeninizdən istifadə edin."
|
||||
}
|
40
lang/id.json
Normal file
40
lang/id.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"auth.login": "Masuk",
|
||||
"auth.login.authentik": "Masuk dengan Authentik",
|
||||
"auth.login.azure": "Masuk dengan Microsoft",
|
||||
"auth.login.bitbucket": "Masuk dengan Bitbucket",
|
||||
"auth.login.github": "Masuk dengan GitHub",
|
||||
"auth.login.gitlab": "Masuk dengan Gitlab",
|
||||
"auth.login.google": "Masuk dengan Google",
|
||||
"auth.login.infomaniak": "Masuk dengan Infomaniak",
|
||||
"auth.already_registered": "Sudah terdaftar?",
|
||||
"auth.confirm_password": "Konfirmasi kata sandi",
|
||||
"auth.forgot_password": "Lupa kata sandi",
|
||||
"auth.forgot_password_send_email": "Kirim email reset kata sandi",
|
||||
"auth.register_now": "Daftar",
|
||||
"auth.logout": "Keluar",
|
||||
"auth.register": "Daftar",
|
||||
"auth.registration_disabled": "Pendaftaran dinonaktifkan. Harap hubungi administrator.",
|
||||
"auth.reset_password": "Reset kata sandi",
|
||||
"auth.failed": "Kredensial ini tidak cocok dengan catatan kami.",
|
||||
"auth.failed.callback": "Gagal memproses callback dari penyedia login.",
|
||||
"auth.failed.password": "Kata sandi yang diberikan salah.",
|
||||
"auth.failed.email": "Kami tidak dapat menemukan pengguna dengan alamat e-mail tersebut.",
|
||||
"auth.throttle": "Terlalu banyak percobaan login. Silakan coba lagi dalam :seconds detik.",
|
||||
"input.name": "Nama",
|
||||
"input.email": "Email",
|
||||
"input.password": "Kata sandi",
|
||||
"input.password.again": "Kata sandi lagi",
|
||||
"input.code": "Kode sekali pakai",
|
||||
"input.recovery_code": "Kode pemulihan",
|
||||
"button.save": "Simpan",
|
||||
"repository.url": "<span class='text-helper'>Contoh</span><br>Untuk repositori Publik, gunakan <span class='text-helper'>https://...</span>.<br>Untuk repositori Privat, gunakan <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples cabang <span class='text-helper'>main</span> akan dipilih<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify cabang <span class='text-helper'>nodejs-fastify</span> akan dipilih.<br>https://gitea.com/sedlav/expressjs.git cabang <span class='text-helper'>main</span> akan dipilih.<br>https://gitlab.com/andrasbacsai/nodejs-example.git cabang <span class='text-helper'>main</span> akan dipilih.",
|
||||
"service.stop": "Layanan ini akan dihentikan.",
|
||||
"resource.docker_cleanup": "Jalankan Pembersihan Docker (hapus gambar yang tidak digunakan dan cache builder).",
|
||||
"resource.non_persistent": "Semua data non-persisten akan dihapus.",
|
||||
"resource.delete_volumes": "Hapus permanen semua volume yang terkait dengan sumber daya ini.",
|
||||
"resource.delete_connected_networks": "Hapus permanen semua jaringan non-predefined yang terkait dengan sumber daya ini.",
|
||||
"resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.",
|
||||
"database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.",
|
||||
"warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https <span class='font-bold text-red-500 dark:text-red-500'>TIDAK</span> direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal). <br><br>Gunakan domain Anda sendiri sebagai gantinya."
|
||||
}
|
40
lang/pt-br.json
Normal file
40
lang/pt-br.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"auth.login": "Entrar",
|
||||
"auth.login.authentik": "Entrar com Authentik",
|
||||
"auth.login.azure": "Entrar com Microsoft",
|
||||
"auth.login.bitbucket": "Entrar com Bitbucket",
|
||||
"auth.login.github": "Entrar com GitHub",
|
||||
"auth.login.gitlab": "Entrar com Gitlab",
|
||||
"auth.login.google": "Entrar com Google",
|
||||
"auth.login.infomaniak": "Entrar com Infomaniak",
|
||||
"auth.already_registered": "Já tem uma conta?",
|
||||
"auth.confirm_password": "Confirmar senha",
|
||||
"auth.forgot_password": "Esqueceu a senha",
|
||||
"auth.forgot_password_send_email": "Enviar e-mail para redefinir senha",
|
||||
"auth.register_now": "Cadastre-se",
|
||||
"auth.logout": "Sair",
|
||||
"auth.register": "Cadastrar",
|
||||
"auth.registration_disabled": "O registro está desativado. Por favor, contate o administrador.",
|
||||
"auth.reset_password": "Redefinir senha",
|
||||
"auth.failed": "Essas credenciais não correspondem aos nossos registros.",
|
||||
"auth.failed.callback": "Falha ao processar o callback do provedor de login.",
|
||||
"auth.failed.password": "A senha fornecida está incorreta.",
|
||||
"auth.failed.email": "Não encontramos nenhum usuário com esse endereço de e-mail.",
|
||||
"auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
|
||||
"input.name": "Nome",
|
||||
"input.email": "E-mail",
|
||||
"input.password": "Senha",
|
||||
"input.password.again": "Senha novamente",
|
||||
"input.code": "Código de uso único",
|
||||
"input.recovery_code": "Código de recuperação",
|
||||
"button.save": "Salvar",
|
||||
"repository.url": "<span class='text-helper'>Exemplos</span><br>Para repositórios públicos, use <span class='text-helper'>https://...</span>.<br>Para repositórios privados, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch será selecionado<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch será selecionado.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch será selecionado.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch será selecionado.",
|
||||
"service.stop": "Este serviço será parado.",
|
||||
"resource.docker_cleanup": "Executar limpeza do Docker (remover imagens não utilizadas e cache de build).",
|
||||
"resource.non_persistent": "Todos os dados não persistentes serão excluídos.",
|
||||
"resource.delete_volumes": "Excluir permanentemente todos os volumes associados a este recurso.",
|
||||
"resource.delete_connected_networks": "Excluir permanentemente todas as redes não predefinidas associadas a este recurso.",
|
||||
"resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.",
|
||||
"database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.",
|
||||
"warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https <span class='dark:text-red-500 text-red-500 font-bold'>NÃO</span> é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará). <br><br>Use seu próprio domínio em vez disso."
|
||||
}
|
10
lang/tr.json
10
lang/tr.json
@@ -27,5 +27,13 @@
|
||||
"input.code": "Tek Kullanımlık Kod",
|
||||
"input.recovery_code": "Kurtarma Kodu",
|
||||
"button.save": "Kaydet",
|
||||
"repository.url": "<span class='text-helper'>Örnekler</span><br>Halka açık depolar için <span class='text-helper'>https://...</span> kullanın.<br>Özel depolar için <span class='text-helper'>git@...</span> kullanın.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> dalı seçilecek<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> dalı seçilecek.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> dalı seçilecek.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> dalı seçilecek."
|
||||
"repository.url": "<span class='text-helper'>Örnekler</span><br>Halka açık depolar için <span class='text-helper'>https://...</span> kullanın.<br>Özel depolar için <span class='text-helper'>git@...</span> kullanın.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> dalı seçilecek<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> dalı seçilecek.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> dalı seçilecek.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> dalı seçilecek.",
|
||||
"service.stop": "Bu servis durdurulacak.",
|
||||
"resource.docker_cleanup": "Docker temizliği çalıştır (kullanılmayan imajları ve oluşturucu önbelleğini kaldır).",
|
||||
"resource.non_persistent": "Tüm kalıcı olmayan veriler silinecek.",
|
||||
"resource.delete_volumes": "Bu kaynakla ilişkili tüm hacimler kalıcı olarak silinecek.",
|
||||
"resource.delete_connected_networks": "Bu kaynakla ilişkili önceden tanımlanmamış tüm ağlar kalıcı olarak silinecek.",
|
||||
"resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.",
|
||||
"database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.",
|
||||
"warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https <span class='dark:text-red-500 text-red-500 font-bold'>ÖNERİLMEZ</span>, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur). <br><br>Bunun yerine kendi domaininizi kullanın."
|
||||
}
|
||||
|
74
openapi.json
74
openapi.json
@@ -2771,80 +2771,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/execute": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Execute Command",
|
||||
"description": "Execute a command on the application's current container.",
|
||||
"operationId": "execute-command-application",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Command to execute.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command to execute."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Execute a command on the application's current container.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Command executed."
|
||||
},
|
||||
"response": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/databases": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
46
openapi.yaml
46
openapi.yaml
@@ -1905,52 +1905,6 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/execute':
|
||||
post:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Execute Command'
|
||||
description: "Execute a command on the application's current container."
|
||||
operationId: execute-command-application
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
description: 'Command to execute.'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
description: 'Command to execute.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: "Execute a command on the application's current container."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Command executed.' }
|
||||
response: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/databases:
|
||||
get:
|
||||
tags:
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.403"
|
||||
"version": "4.0.0-beta.404"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.404"
|
||||
"version": "4.0.0-beta.405"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.8"
|
||||
|
@@ -1,8 +1,9 @@
|
||||
@props(['text'])
|
||||
|
||||
<div class="relative" x-data="{ copied: false }">
|
||||
<div class="relative" x-data="{ copied: false, isSecure: window.isSecureContext }">
|
||||
<input type="text" value="{{ $text }}" readonly class="input">
|
||||
<button
|
||||
x-show="isSecure"
|
||||
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
|
||||
title="Copy to clipboard">
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Backup</h2>
|
||||
@if (isset($database))
|
||||
@if (isset($database) && $server->isFunctional())
|
||||
<x-forms.button type="submit" wire:click="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@@ -14,26 +14,39 @@
|
||||
</div>
|
||||
<div class="pb-4">Backup configuration for Coolify instance.</div>
|
||||
<div>
|
||||
@if (isset($database) && isset($backup))
|
||||
<div class="flex flex-col gap-3 pb-4">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="UUID" readonly id="uuid" />
|
||||
<x-forms.input label="Name" readonly id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
@if ($server->isFunctional())
|
||||
@if (isset($database) && isset($backup))
|
||||
<div class="flex flex-col gap-3 pb-4">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="UUID" readonly id="uuid" />
|
||||
<x-forms.input label="Name" readonly id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="User" readonly id="postgres_user" />
|
||||
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="User" readonly id="postgres_user" />
|
||||
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
|
||||
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
|
||||
<div class="py-4">
|
||||
<livewire:project.database.backup-executions :backup="$backup" />
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
|
||||
<div class="py-4">
|
||||
<livewire:project.database.backup-executions :backup="$backup" />
|
||||
</div>
|
||||
@else
|
||||
To configure automatic backup for your Coolify instance, you first need to add a database resource
|
||||
into Coolify.
|
||||
<x-forms.button class="mt-2" wire:click="addCoolifyDatabase">Configure Backup</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
To configure automatic backup for your Coolify instance, you first need to add a database resource
|
||||
into Coolify.
|
||||
<x-forms.button class="mt-2" wire:click="addCoolifyDatabase">Configure Backup</x-forms.button>
|
||||
<div class="p-6 bg-red-500/10 rounded-lg border border-red-500/20">
|
||||
<div class="text-red-500 font-medium mb-4">
|
||||
Instance Backup is currently disabled because the localhost server is not properly validated.
|
||||
Please validate your server to enable Instance Backup.
|
||||
</div>
|
||||
<a href="{{ route('server.show', [$server->uuid]) }}"
|
||||
class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline">
|
||||
Go to Server Settings to Validate
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -39,9 +39,15 @@ if ! docker network inspect coolify >/dev/null 2>&1; then
|
||||
fi
|
||||
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>$LOGFILE
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
fi
|
||||
|
@@ -3,17 +3,18 @@
|
||||
# tags: python, error-tracking, django, mysql
|
||||
# logo: svgs/bugsink.svg
|
||||
# port: 8000
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: 'mysql:latest'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}'
|
||||
- 'MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink}'
|
||||
- 'MYSQL_USER=${SERVICE_USER_BUGSINK}'
|
||||
- 'MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK}'
|
||||
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink}
|
||||
- MYSQL_USER=${SERVICE_USER_BUGSINK}
|
||||
- MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK}
|
||||
volumes:
|
||||
- 'my-datavolume:/var/lib/mysql'
|
||||
- my-datavolume:/var/lib/mysql
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -29,13 +30,15 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECRET_KEY=$SERVICE_PASSWORD_64_BUGSINK
|
||||
- 'CREATE_SUPERUSER=admin:${SERVICE_PASSWORD_BUGSINK}'
|
||||
- CREATE_SUPERUSER=admin:$SERVICE_PASSWORD_BUGSINK
|
||||
- SERVICE_FQDN_BUGSINK_8000
|
||||
- 'BASE_URL=${SERVICE_FQDN_BUGSINK_8000}'
|
||||
- 'DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}'
|
||||
- BASE_URL=$SERVICE_FQDN_BUGSINK_8000
|
||||
- DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
my-datavolume:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8000/\").raise_for_status()'"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
@@ -271,7 +271,7 @@
|
||||
"bugsink": {
|
||||
"documentation": "https://www.bugsink.com/docs/?utm_source=coolify.io",
|
||||
"slogan": "Self-hosted Error Tracking",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiR7U0VSVklDRV9QQVNTV09SRF9CVUdTSU5LfScKICAgICAgLSBTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX0JVR1NJTktfODAwMH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CnZvbHVtZXM6CiAgbXktZGF0YXZvbHVtZTogbnVsbAo=",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9GUUROX0JVR1NJTktfODAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3B5dGhvbiAtYyAnJ2ltcG9ydCByZXF1ZXN0czsgcmVxdWVzdHMuZ2V0KCJodHRwOi8vbG9jYWxob3N0OjgwMDAvIikucmFpc2VfZm9yX3N0YXR1cygpJycnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"tags": [
|
||||
"python",
|
||||
"error-tracking",
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.403"
|
||||
"version": "4.0.0-beta.404"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.404"
|
||||
"version": "4.0.0-beta.405"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.8"
|
||||
|
Reference in New Issue
Block a user