Compare commits

...

117 Commits

Author SHA1 Message Date
Andras Bacsai
59d6818f70 Merge pull request #1339 from coollabsio/next
v4.0.0-beta.93
2023-10-18 11:30:40 +02:00
Andras Bacsai
7678cd47df fix: add config_hash if its null (old deployments) 2023-10-18 11:26:01 +02:00
Andras Bacsai
b101fbacd4 fix: do not show configuration changed if config_hash is null 2023-10-18 11:22:56 +02:00
Andras Bacsai
a61a86dc3b feat: show if config is not applied 2023-10-18 11:20:40 +02:00
Andras Bacsai
0b3cde44c3 feat: able to customize docker labels on applications 2023-10-18 10:32:08 +02:00
Andras Bacsai
6b302ab786 Add restarting indicator to resources 2023-10-18 09:03:14 +02:00
Andras Bacsai
3c623f13e2 revert version 2023-10-17 20:54:54 +02:00
Andras Bacsai
da54c24e8d fix: setup:dev script & contribution guide 2023-10-17 20:54:26 +02:00
Andras Bacsai
1e39c3d5ab Merge pull request #1338 from coollabsio/next
v4.0.0-beta.92
2023-10-17 19:03:04 +02:00
Andras Bacsai
6071412986 fix: proxy start process 2023-10-17 19:00:23 +02:00
Andras Bacsai
ba7148206a Merge pull request #1336 from coollabsio/next
v4.0.0-beta.91
2023-10-17 15:41:30 +02:00
Andras Bacsai
59c5b22e6c fix: always start proxy if not NONE is selected 2023-10-17 15:40:47 +02:00
Andras Bacsai
be7f2ad9c4 ui: add helper to service domains 2023-10-17 15:34:20 +02:00
Andras Bacsai
62295ef573 Merge pull request #1335 from coollabsio/next
v4.0.0-beta.90
2023-10-17 14:45:26 +02:00
Andras Bacsai
ceb9fcf3b6 service: wordpress 2023-10-17 14:44:25 +02:00
Andras Bacsai
60282f7b6c fix: only include config.json if its exists and a file 2023-10-17 14:23:07 +02:00
Andras Bacsai
f14b0a3411 Merge pull request #1334 from coollabsio/next
v4.0.0-beta.89
2023-10-17 14:06:12 +02:00
Andras Bacsai
30af317bd9 fix: show docker build logs 2023-10-17 14:04:21 +02:00
Andras Bacsai
95faa1c3ad fix: noindex meta tag 2023-10-17 13:28:33 +02:00
Andras Bacsai
fb280afe41 Merge pull request #1332 from coollabsio/next
v4.0.0-beta.88
2023-10-17 12:41:45 +02:00
Andras Bacsai
fd488a561a feat: use docker login credentials from server 2023-10-17 12:35:04 +02:00
Andras Bacsai
ab57a5d8ef Merge pull request #1331 from coollabsio/next
v4.0.0-beta.87
2023-10-17 12:14:03 +02:00
Andras Bacsai
f5ae222a6e fix: add internal domain names during build process 2023-10-17 11:23:49 +02:00
Andras Bacsai
5d95d8b79a fix: cancel any deployments + queue next 2023-10-17 11:10:33 +02:00
Andras Bacsai
fbb5f2ca2e fix: generate fqdn if you deleted a service app, but it requires fqdn 2023-10-17 10:37:39 +02:00
Andras Bacsai
16cbca36c1 add trademark policy 2023-10-17 10:37:26 +02:00
Andras Bacsai
24a578bedb Merge pull request #1327 from theh2so4/main
[+] Update
2023-10-17 10:31:25 +02:00
Andras Bacsai
36dc479772 fix: service status check is a bit better 2023-10-17 10:17:03 +02:00
Andras Bacsai
83d6e488e4 Merge pull request #1330 from seii/fix/raspbian-support
Add Raspbian support to install.sh
2023-10-17 09:50:57 +02:00
Seii
a4d358d512 Add Raspbian support to install.sh 2023-10-16 22:11:51 -06:00
TheH2SO4
c0c197101d [+] Update
💄 **Styling**:

-> ℹ️ **Alphabetical order**: Changed the order of the templates and set them on a alphabetical order.
-> ℹ️ **More accurate descriptions**: Created more accurate descriptions for the templates: `Appsmith, Appwrite, Fider, Ghost, Umami` and `Uptime Kuma`.
2023-10-15 23:48:33 +02:00
Andras Bacsai
62e39ccc7f Merge pull request #1326 from coollabsio/next
v4.0.0-beta.86
2023-10-15 16:56:21 +02:00
Andras Bacsai
a88a016137 fix: build image before starting dockerfile buildpacks 2023-10-15 16:54:16 +02:00
Andras Bacsai
f4c8986ab3 Merge pull request #1324 from coollabsio/next
v4.0.0-beta.85
2023-10-14 17:40:08 +02:00
Andras Bacsai
e286eae53b Merge pull request #1322 from scmmishra/fix/redis-default-url
fix: generated redis URL string
2023-10-14 17:36:48 +02:00
Andras Bacsai
bc3e59e4ef fix: delete resource if there was an error
fix: do not refresh on containerStatusJob (db view)
2023-10-14 17:26:16 +02:00
Andras Bacsai
17ebc650c9 Merge pull request #1323 from coollabsio/next
v4.0.0-beta.84
2023-10-14 14:24:20 +02:00
Andras Bacsai
0ef386b4a8 fix: stopping a resource is now job based
ui: show status on project
2023-10-14 14:22:07 +02:00
Shivam Mishra
26fce85bb0 fix: redis URL generated 2023-10-14 12:10:12 +05:30
Andras Bacsai
2a079e3365 Merge pull request #1321 from coollabsio/next
v4.0.0-beta.83
2023-10-13 21:54:26 +02:00
Andras Bacsai
5fb5ed75c4 Merge pull request #1315 from scmmishra/patch-1
fix: docker hub URL for redis
2023-10-13 21:46:46 +02:00
Andras Bacsai
a2008fe9d1 typo 2023-10-13 21:44:04 +02:00
Andras Bacsai
3c96485e3d fix: custom dockerfile location for dockerfile buildpack
fix: able to use custom port for git cloning
2023-10-13 21:39:49 +02:00
Andras Bacsai
1c97d47ea0 fix: turn off static deployment if you switch buildpacks 2023-10-13 21:08:59 +02:00
Shivam Mishra
8f8f5878dd fix: docker hub URL 2023-10-13 22:34:46 +05:30
Andras Bacsai
6e6f39dc1f Merge pull request #1314 from coollabsio/next
v4.0.0-beta.82
2023-10-13 16:38:52 +02:00
Andras Bacsai
d2d1f984e1 fix 2023-10-13 16:32:59 +02:00
Andras Bacsai
d635e5dbae fix: backup database one-by-one. 2023-10-13 15:45:24 +02:00
Andras Bacsai
49c56524e1 fix: dev containerjobs 2023-10-13 15:17:04 +02:00
Andras Bacsai
6ced607f2a fix: timeout for instant remote processes 2023-10-13 15:16:52 +02:00
Andras Bacsai
aaa2febef4 fix: docker cleanup jobs 2023-10-13 15:16:33 +02:00
Andras Bacsai
10e6eddcfe fix: db labels 2023-10-13 15:16:22 +02:00
Andras Bacsai
2639bf92ad fix: traefik dashboard ip 2023-10-13 14:35:02 +02:00
Andras Bacsai
59eae3a44e fix: proxy check for ports, do not kill anything listening on port 80/443 2023-10-13 14:25:30 +02:00
Andras Bacsai
5aa8ccfcf4 link to default redis conf 2023-10-13 09:42:38 +02:00
Andras Bacsai
aea7cc9638 fix: show database logs in case of its not healthy and running 2023-10-13 09:40:37 +02:00
Andras Bacsai
c970907c73 fix: no backup for redis 2023-10-13 09:39:40 +02:00
Andras Bacsai
38c6c1ee40 fix: urls should be password fields 2023-10-13 09:36:37 +02:00
Andras Bacsai
a6118f5daf fix 2023-10-13 09:34:11 +02:00
Andras Bacsai
b196c138d9 fix: server ip could be hostname in self-hosted 2023-10-13 09:31:44 +02:00
Andras Bacsai
8f9949160c feat: add custom redis conf 2023-10-12 17:29:29 +02:00
Andras Bacsai
beae0b545f init: redis 2023-10-12 17:18:33 +02:00
Andras Bacsai
b8dd7704b3 update resource delete command 2023-10-12 13:35:57 +02:00
Andras Bacsai
d913be66e6 Merge pull request #1312 from coollabsio/next
v4.0.0-beta.81
2023-10-12 12:23:03 +02:00
Andras Bacsai
63de538879 seeder update 2023-10-12 12:18:26 +02:00
Andras Bacsai
1d733b2282 seeder 2023-10-12 12:11:54 +02:00
Andras Bacsai
1d0ad51fdf fix: dockerfile location feature 2023-10-12 12:01:09 +02:00
Andras Bacsai
83d00bbe3c fix: make sure to use IP address 2023-10-12 11:41:37 +02:00
Andras Bacsai
c422b4dbcf fix: isCloud in production seeder 2023-10-12 11:36:44 +02:00
Andras Bacsai
767fd334dd Merge pull request #1310 from coollabsio/next
v4.0.0-beta.80
2023-10-12 09:47:39 +02:00
Andras Bacsai
972223f01b disable docker_compose deployments 2023-10-12 09:30:27 +02:00
Andras Bacsai
9318cac189 fix: service status check
fix: containerStatusJob
fix: service form
2023-10-12 09:12:46 +02:00
Andras Bacsai
7aa991fd7c fix: service check status 10 sec 2023-10-12 08:58:08 +02:00
Andras Bacsai
5c27f43b3d move autoupdate job to actions 2023-10-12 08:56:29 +02:00
Andras Bacsai
a2f4d4ed6d fix: make sure proxy wont start in NONE mode 2023-10-12 08:51:32 +02:00
Andras Bacsai
6aca2740fb fix 2023-10-11 15:46:59 +02:00
Andras Bacsai
cd13b5b83e version++ 2023-10-11 15:39:27 +02:00
Andras Bacsai
758dbafbf1 Merge pull request #1308 from coollabsio/next
v4.0.0-beta.79
2023-10-11 15:27:21 +02:00
Andras Bacsai
f6663661df disallow robots 2023-10-11 15:07:00 +02:00
Andras Bacsai
9666099408 commit 2023-10-11 14:43:34 +02:00
Andras Bacsai
d382af6860 fix 2023-10-11 14:40:41 +02:00
Andras Bacsai
4905454269 hm 2023-10-11 14:33:18 +02:00
Andras Bacsai
ed8bd37230 disallow robots 2023-10-11 14:31:59 +02:00
Andras Bacsai
ec1a7aa893 Merge pull request #1307 from coollabsio/next
v4.0.0-beta.78
2023-10-11 14:25:27 +02:00
Andras Bacsai
62adf2c5dc fix: boarding + verification 2023-10-11 14:24:19 +02:00
Andras Bacsai
3e4538de98 update command 2023-10-11 14:12:02 +02:00
Andras Bacsai
5a7b16ea5f command: delete server 2023-10-11 14:04:21 +02:00
Andras Bacsai
aa7bc40f85 fix: send unreachable/revived notifications 2023-10-11 13:52:46 +02:00
Andras Bacsai
4fd83dc727 version++ 2023-10-11 13:51:30 +02:00
Andras Bacsai
0e451f87a9 Merge pull request #1305 from coollabsio/next
v4.0.0-beta.77
2023-10-11 13:50:56 +02:00
Andras Bacsai
0b0ae55f0b fix 2023-10-11 13:47:14 +02:00
Andras Bacsai
40ec3d9753 fix 2023-10-11 13:34:51 +02:00
Andras Bacsai
9535c8df29 fix: check localhost connection 2023-10-11 13:30:36 +02:00
Andras Bacsai
6ca1d36d5d fix: cannot remove localhost 2023-10-11 13:12:29 +02:00
Andras Bacsai
f5d16c46cb version++ 2023-10-11 13:11:52 +02:00
Andras Bacsai
725c3fd547 Merge pull request #1304 from coollabsio/next
v4.0.0-beta.76
2023-10-11 12:57:35 +02:00
Andras Bacsai
dcfcee1db6 fix: public git 2023-10-11 12:56:57 +02:00
Andras Bacsai
9f8c44d96b version++ 2023-10-11 12:33:06 +02:00
Andras Bacsai
5b74fd34f5 fix: instant save build pack change 2023-10-11 12:12:25 +02:00
Andras Bacsai
5a4c9422b2 fix: only require registry image in case of dockerimage bp 2023-10-11 12:10:40 +02:00
Andras Bacsai
81b916724e Merge pull request #1303 from coollabsio/next
v4.0.0-beta.75
2023-10-11 12:06:15 +02:00
Andras Bacsai
9540f60fa2 fix: dashboard goto link 2023-10-11 12:04:30 +02:00
Andras Bacsai
8eb1686125 fix: transactional email link 2023-10-11 12:04:23 +02:00
Andras Bacsai
daf3710a5e fix: add new team button 2023-10-11 12:04:14 +02:00
Andras Bacsai
1067f37e4d fix: deleted team and it is the current one 2023-10-11 12:03:59 +02:00
Andras Bacsai
3b3c0b94e5 version++ 2023-10-11 12:03:39 +02:00
Andras Bacsai
8b0a0d67da Merge pull request #1302 from coollabsio/next
v4.0.0-beta.74
2023-10-11 11:09:19 +02:00
Andras Bacsai
5541c135df feat: proxy logs on the ui 2023-10-11 11:00:40 +02:00
Andras Bacsai
2552cb2208 ui: able to select environment on new resource 2023-10-11 10:19:03 +02:00
Andras Bacsai
f001e9bc34 improve dashboard 2023-10-11 10:08:37 +02:00
Andras Bacsai
f943fdc5be fix: use only ip addresses for servers 2023-10-11 09:57:35 +02:00
Andras Bacsai
a4f1fcba58 move subscription to livewire + show manage subscription button for people already subscribed once 2023-10-11 09:55:05 +02:00
Andras Bacsai
68091b44fc fix: contact link 2023-10-11 09:54:01 +02:00
Andras Bacsai
9f8caac91c dev: coolify proxy access logs exposed in dev 2023-10-11 09:31:30 +02:00
Andras Bacsai
8082dc1a01 fix: use port exposed for reverse proxy 2023-10-11 09:23:31 +02:00
Andras Bacsai
a71cf5bc66 cleanup 2023-10-10 15:36:06 +02:00
Andras Bacsai
0775074509 version++ 2023-10-10 14:34:38 +02:00
126 changed files with 2069 additions and 819 deletions

View File

@@ -27,3 +27,5 @@ You can ask for guidance anytime on our
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`. You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user. Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
Mails are caught by Mailpit: `localhost:8025`

View File

@@ -36,7 +36,7 @@ You can find the installation script [here](./scripts/install.sh).
## Support ## Support
Contact us [here](https://coolify.io/contact). Contact us [here](https://coolify.io/docs/contact).
## Recognitions ## Recognitions

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication
{
use AsAction;
public function handle(Application $application)
{
$server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$server
);
}
}
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
$internalPort = null;
if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:$internalPort;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up --build -d",
], $database->destination->server);
}
}

View File

@@ -6,15 +6,18 @@ use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartPostgresql class StartPostgresql
{ {
use AsAction;
public StandalonePostgresql $database; public StandalonePostgresql $database;
public array $commands = []; public array $commands = [];
public array $init_scripts = []; public array $init_scripts = [];
public string $configuration_dir; public string $configuration_dir;
public function __invoke(Server $server, StandalonePostgresql $database) public function handle(Server $server, StandalonePostgresql $database)
{ {
$this->database = $database; $this->database = $database;
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
@@ -41,6 +44,9 @@ class StartPostgresql
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD-SHELL', 'CMD-SHELL',

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandaloneRedis;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartRedis
{
use AsAction;
public StandaloneRedis $database;
public array $commands = [];
public string $configuration_dir;
public function handle(Server $server, StandaloneRedis $database)
{
$this->database = $database;
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'redis-cli',
'ping'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' /usr/local/etc/redis/redis.conf';
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
return $environment_variables->all();
}
private function add_custom_redis()
{
if (is_null($this->database->redis_conf)) {
return;
}
$filename = 'redis.conf';
$content = $this->database->redis_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
$server = $database->destination->server;
instant_remote_process(
["docker rm -f {$database->uuid}"],
$server
);
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
// TODO: make notification for services
// $database->environment->project->team->notify(new StatusChanged($database));
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$database->is_public = false;
$database->save();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckProxy
{
use AsAction;
public function handle(Server $server, $fromUI = false)
{
if (!$server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");
} else {
return false;
}
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
return true;
}
}

View File

@@ -10,56 +10,47 @@ use Spatie\Activitylog\Models\Activity;
class StartProxy class StartProxy
{ {
use AsAction; use AsAction;
public function handle(Server $server, bool $async = true): Activity|string public function handle(Server $server, bool $async = true): string|Activity
{ {
$commands = collect([]); try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
if ($proxyType === 'none') { $commands = collect([]);
return 'OK'; $proxy_path = get_proxy_path();
} $configuration = CheckConfiguration::run($server);
$proxy_path = get_proxy_path(); if (!$configuration) {
$configuration = CheckConfiguration::run($server); throw new \Exception("Configuration is not synced");
if (!$configuration) { }
throw new \Exception("Configuration is not synced"); SaveConfiguration::run($server, $configuration);
} $docker_compose_yml_base64 = base64_encode($configuration);
SaveConfiguration::run($server, $configuration); $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$commands = $commands->merge([
"apt-get update > /dev/null 2>&1 || true",
"command -v lsof >/dev/null || echo '####### Installing lsof.'",
"command -v lsof >/dev/null || apt install -y lsof",
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc",
"mkdir -p $proxy_path && cd $proxy_path",
"echo '####### Creating Docker Compose file.'",
"echo '####### Pulling docker image.'",
'docker compose pull',
"echo '####### Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'",
"command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true",
"command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true",
"command -v fuser >/dev/null && fuser -k 80/tcp || true",
"command -v fuser >/dev/null && fuser -k 443/tcp || true",
"systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save(); $server->save();
return 'OK'; $commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
return 'OK';
}
} catch(\Throwable $e) {
ray($e);
throw $e;
} }
} }
} }

View File

@@ -2,16 +2,18 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
class UpdateCoolify class UpdateCoolify
{ {
use AsAction;
public ?Server $server = null; public ?Server $server = null;
public ?string $latestVersion = null; public ?string $latestVersion = null;
public ?string $currentVersion = null; public ?string $currentVersion = null;
public function __invoke(bool $force) public function handle(bool $force)
{ {
try { try {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service; use App\Models\Service;
use App\Notifications\Application\StatusChanged;
class StopService class StopService
{ {
@@ -22,5 +23,7 @@ class StopService
} }
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false); instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Application; use App\Models\Application;
use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -34,7 +35,7 @@ class ResourcesDelete extends Command
{ {
$resource = select( $resource = select(
'What resource do you want to delete?', 'What resource do you want to delete?',
['Application', 'Database', 'Service'], ['Application', 'Database', 'Service', 'Server'],
); );
if ($resource === 'Application') { if ($resource === 'Application') {
$this->deleteApplication(); $this->deleteApplication();
@@ -42,6 +43,30 @@ class ResourcesDelete extends Command
$this->deleteDatabase(); $this->deleteDatabase();
} elseif ($resource === 'Service') { } elseif ($resource === 'Service') {
$this->deleteService(); $this->deleteService();
} elseif ($resource === 'Server') {
$this->deleteServer();
}
}
private function deleteServer()
{
$servers = Server::all();
if ($servers->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$serversToDelete = multiselect(
label: 'What server do you want to delete?',
options: $servers->pluck('name', 'id')->sortKeys(),
);
foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
break;
}
$toDelete->delete();
} }
} }
private function deleteApplication() private function deleteApplication()
@@ -53,14 +78,17 @@ class ResourcesDelete extends Command
} }
$applicationsToDelete = multiselect( $applicationsToDelete = multiselect(
'What application do you want to delete?', 'What application do you want to delete?',
$applications->pluck('name')->toArray(), $applications->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($applicationsToDelete as $application) { foreach ($applicationsToDelete as $application) {
$toDelete = $applications->where('name', $application)->first(); ray($application);
$toDelete = $applications->where('id', $application)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
break;
}
$toDelete->delete(); $toDelete->delete();
} }
} }
@@ -73,17 +101,18 @@ class ResourcesDelete extends Command
} }
$databasesToDelete = multiselect( $databasesToDelete = multiselect(
'What database do you want to delete?', 'What database do you want to delete?',
$databases->pluck('name')->toArray(), $databases->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($databasesToDelete as $database) { foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first(); $toDelete = $databases->where('id', $database)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete(); $toDelete->delete();
} }
} }
private function deleteService() private function deleteService()
{ {
@@ -94,14 +123,16 @@ class ResourcesDelete extends Command
} }
$servicesToDelete = multiselect( $servicesToDelete = multiselect(
'What service do you want to delete?', 'What service do you want to delete?',
$services->pluck('name')->toArray(), $services->pluck('name', 'id')->sortKeys(),
); );
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($servicesToDelete as $service) { foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first(); $toDelete = $services->where('id', $service)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete(); $toDelete->delete();
} }
} }

View File

@@ -28,11 +28,11 @@ class Kernel extends ConsoleKernel
// $this->check_scheduled_backups($schedule); // $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule); $this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule);
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule); $this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);

View File

@@ -46,15 +46,6 @@ class Controller extends BaseController
} }
return redirect()->route('login')->with('error', 'Invalid credentials.'); return redirect()->route('login')->with('error', 'Invalid credentials.');
} }
public function subscription()
{
if (!isCloud()) {
abort(404);
}
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license() public function license()
{ {

View File

@@ -19,7 +19,7 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -37,7 +37,7 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -64,10 +64,18 @@ class DatabaseController extends Controller
if (!$environment) { if (!$environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first(); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) { if (!$database) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
return view('project.database.backups.all', [ return view('project.database.backups.all', [
'database' => $database, 'database' => $database,
's3s' => currentTeam()->s3s, 's3s' => currentTeam()->s3s,

View File

@@ -59,11 +59,15 @@ class ProjectController extends Controller
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (in_array($type, DATABASE_TYPES)) { if (in_array($type, DATABASE_TYPES)) {
$standalone_postgresql = create_standalone_postgresql($environment->id, $destination_uuid); if ($type->value() === "postgresql") {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid, 'database_uuid' => $database->uuid,
]); ]);
} }
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {

View File

@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{ {
$this->validate([ $this->validate([
'remoteServerName' => 'required', 'remoteServerName' => 'required',
'remoteServerHost' => 'required', 'remoteServerHost' => 'required|ip',
'remoteServerPort' => 'required|integer', 'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required', 'remoteServerUser' => 'required',
]); ]);

View File

@@ -11,19 +11,11 @@ class Dashboard extends Component
{ {
public $projects = []; public $projects = [];
public $servers = []; public $servers = [];
public int $s3s = 0;
public int $resources = 0;
public function mount() public function mount()
{ {
$this->servers = Server::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count(); $this->projects = Project::ownedByCurrentTeam()->get();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects;
} }
// public function getIptables() // public function getIptables()
// { // {

View File

@@ -60,12 +60,16 @@ class DeploymentNavbar extends Component
$previous_logs[] = $new_log_entry; $previous_logs[] = $new_log_entry;
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]); ]);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
queue_next_deployment($this->application);
} }
} }
} }

View File

@@ -3,12 +3,9 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component class General extends Component
{ {
@@ -23,6 +20,10 @@ class General extends Component
public ?string $git_commit_sha = null; public ?string $git_commit_sha = null;
public string $build_pack; public string $build_pack;
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled; public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled; public bool $is_git_lfs_enabled;
@@ -52,6 +53,7 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -73,9 +75,47 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
]; ];
public function mount()
{
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (is_null(data_get($this->application, 'custom_labels'))) {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode(",", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function instantSave() public function instantSave()
{ {
// @TODO: find another way - if possible // @TODO: find another way - if possible
@@ -95,36 +135,41 @@ class General extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
public function getWildcardDomain() { public function getWildcardDomain()
{
$server = data_get($this->application, 'destination.server'); $server = data_get($this->application, 'destination.server');
if ($server) { if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
} }
} }
public function mount() public function resetDefaultLabels($showToaster = true)
{ {
if (data_get($this->application,'settings')) { $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->is_static = $this->application->settings->is_static; $this->submit($showToaster);
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
} }
public function submit() public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->emit('success', 'Labels reseted to default!');
}
public function submit($showToaster = true)
{ {
try { try {
$this->validate(); $this->validate();
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required',
]);
}
if (data_get($this->application, 'fqdn')) { if (data_get($this->application, 'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower(); return Str::of($domain)->trim()->lower();
@@ -143,10 +188,17 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') { if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/'); $this->application->publish_directory = rtrim($this->application->publish_directory, '/');
} }
if (gettype($this->customLabels) === 'string') {
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Application; use App\Models\Application;
use Livewire\Component; use Livewire\Component;
@@ -59,22 +60,9 @@ class Heading extends Component
public function stop() public function stop()
{ {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); StopApplication::run($this->application);
if ($containers->count() === 0) { $this->application->status = 'exited';
return; $this->application->save();
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'exited';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
$this->application->refresh(); $this->application->refresh();
} }
} }

View File

@@ -17,6 +17,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'required|integer|min:1', 'backup.number_of_backups_locally' => 'required|integer|min:1',
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'backup.enabled' => 'Enabled', 'backup.enabled' => 'Enabled',
@@ -24,6 +25,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'Number of Backups Locally', 'backup.number_of_backups_locally' => 'Number of Backups Locally',
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
]; ];
protected $messages = [ protected $messages = [
'backup.s3_storage_id' => 'Select a S3 Storage', 'backup.s3_storage_id' => 'Select a S3 Storage',
@@ -37,7 +39,6 @@ class BackupEdit extends Component
} }
} }
public function delete() public function delete()
{ {
// TODO: Delete backup from server and add a confirmation modal // TODO: Delete backup from server and add a confirmation modal
@@ -49,6 +50,7 @@ class BackupEdit extends Component
{ {
try { try {
$this->custom_validate(); $this->custom_validate();
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');
@@ -71,9 +73,11 @@ class BackupEdit extends Component
public function submit() public function submit()
{ {
ray($this->backup->s3_storage_id);
try { try {
$this->custom_validate(); $this->custom_validate();
if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
$this->backup->databases_to_backup = null;
}
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');

View File

@@ -13,6 +13,6 @@ class BackupNow extends Component
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
} }

View File

@@ -32,7 +32,7 @@ class CreateScheduledBackup extends Component
$this->emit('error', 'Invalid Cron / Human expression.'); $this->emit('error', 'Invalid Cron / Human expression.');
return; return;
} }
ScheduledDatabaseBackup::create([ $payload = [
'enabled' => true, 'enabled' => true,
'frequency' => $this->frequency, 'frequency' => $this->frequency,
'save_s3' => $this->save_s3, 'save_s3' => $this->save_s3,
@@ -40,7 +40,11 @@ class CreateScheduledBackup extends Component
'database_id' => $this->database->id, 'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(), 'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ];
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
}
ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups'); $this->emit('refreshScheduledBackups');
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);

View File

@@ -3,6 +3,8 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use Livewire\Component; use Livewire\Component;
@@ -35,24 +37,20 @@ class Heading extends Component
public function stop() public function stop()
{ {
instant_remote_process( StopDatabase::run($this->database);
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'exited'; $this->database->status = 'exited';
$this->database->save(); $this->database->save();
$this->check_status(); $this->check_status();
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
} }
public function start() public function start()
{ {
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database); $activity = StartPostgresql::run($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
} }

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Project\Database\Postgresql; namespace App\Http\Livewire\Project\Database\Postgresql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Exception; use Exception;
use Livewire\Component; use Livewire\Component;
@@ -67,10 +69,10 @@ class General extends Component
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...'); $this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database); StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
stopPostgresProxy($this->database); StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.'); $this->emit('success', 'Database is no longer publicly accessible.');
} }
$this->getDbUrl(); $this->getDbUrl();

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Http\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneRedis;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneRedis $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.redis_conf' => 'nullable',
'database.redis_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.redis_conf' => 'Redis Configuration',
'database.redis_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
try {
$this->validate();
if ($this->database->redis_conf === "") {
$this->database->redis_conf = null;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0";
} else {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->uuid}:6379/0";
}
}
public function render()
{
return view('livewire.project.database.redis.general');
}
}

View File

@@ -98,7 +98,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'git_full_url' => $this->git_repository,
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
@@ -112,7 +111,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'git_full_url' => "git@$this->git_host:$this->git_repository.git",
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
@@ -158,6 +156,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->git_host = $this->repository_url_parsed->getHost(); $this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git'); $this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
} else {
$this->git_repository = $this->repository_url;
} }
$this->git_source = 'other'; $this->git_source = 'other';
} }

View File

@@ -72,10 +72,14 @@ class PublicGitRepository extends Component
public function load_branch() public function load_branch()
{ {
try { try {
$this->branch_found = false;
$this->validate([ $this->validate([
'repository_url' => 'required|url' 'repository_url' => 'required|url'
]); ]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
try {
$this->branch_found = false;
$this->get_git_source(); $this->get_git_source();
$this->get_branch(); $this->get_branch();
$this->selected_branch = $this->git_branch; $this->selected_branch = $this->git_branch;

View File

@@ -2,12 +2,11 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Countable; use Countable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Select extends Component class Select extends Component
@@ -24,7 +23,8 @@ class Select extends Component
public Collection|array $services = []; public Collection|array $services = [];
public bool $loadingServices = true; public bool $loadingServices = true;
public bool $loading = false; public bool $loading = false;
public $environments = [];
public ?string $selectedEnvironment = null;
public ?string $existingPostgresqlUrl = null; public ?string $existingPostgresqlUrl = null;
protected $queryString = [ protected $queryString = [
@@ -37,8 +37,18 @@ class Select extends Component
if (isDev()) { if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432'; $this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
} }
$projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name');
} }
public function updatedSelectedEnvironment()
{
return redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
]);
}
// public function addExistingPostgresql() // public function addExistingPostgresql()
// { // {
// try { // try {

View File

@@ -13,13 +13,7 @@ class Index extends Component
public $databases; public $databases;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $rules = [ protected $listeners = ["refreshStacks","checkStatus"];
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
protected $listeners = ["saveCompose"];
public function render() public function render()
{ {
return view('livewire.project.service.index'); return view('livewire.project.service.index');
@@ -32,17 +26,12 @@ class Index extends Component
$this->applications = $this->service->applications->sort(); $this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort(); $this->databases = $this->service->databases->sort();
} }
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function checkStatus() public function checkStatus()
{ {
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack(); $this->refreshStacks();
} }
public function refreshStack() public function refreshStacks()
{ {
$this->applications = $this->service->applications->sort(); $this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) { $this->applications->each(function ($application) {
@@ -53,21 +42,4 @@ class Index extends Component
$database->refresh(); $database->refresh();
}); });
} }
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->refreshStack();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class Modal extends Component class Modal extends Component
{ {
public function serviceStatusUpdated() { public function checkStatus() {
$this->emit('serviceStatusUpdated'); $this->emit('checkStatus');
} }
public function render() public function render()
{ {

View File

@@ -13,20 +13,15 @@ class Navbar extends Component
public Service $service; public Service $service;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render() public function render()
{ {
return view('livewire.project.service.navbar'); return view('livewire.project.service.navbar');
} }
public function serviceStatusUpdated()
public function checkStatus()
{ {
$this->check_status(); $this->emit('checkStatus');
}
public function check_status()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->service->refresh();
} }
public function deploy() public function deploy()
{ {
@@ -39,5 +34,6 @@ class Navbar extends Component
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh(); $this->service->refresh();
$this->emit('success', 'Service stopped successfully.'); $this->emit('success', 'Service stopped successfully.');
$this->checkStatus();
} }
} }

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class StackForm extends Component
{
public $service;
protected $listeners = ["saveCompose"];
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->emit('refreshStacks');
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.service.stack-form');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Shared; namespace App\Http\Livewire\Project\Shared;
use App\Actions\Service\StopService; use App\Jobs\StopResourceJob;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -10,7 +10,7 @@ class Danger extends Component
{ {
public $resource; public $resource;
public array $parameters; public array $parameters;
public string|null $modalId = null; public ?string $modalId = null;
public function mount() public function mount()
{ {
@@ -20,22 +20,8 @@ class Danger extends Component
public function delete() public function delete()
{ {
// Should be queued
try { try {
if ($this->resource->type() === 'service') { StopResourceJob::dispatchSync($this->resource);
$server = $this->resource->server;
StopService::run($this->resource);
} else {
$destination = data_get($this->resource, 'destination');
if ($destination) {
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server;
}
if ($this->resource->destination->server->isFunctional()) {
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
}
}
$this->resource->delete();
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'] 'environment_name' => $this->parameters['environment_name']

View File

@@ -75,6 +75,9 @@ class All extends Component
case 'standalone-postgresql': case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id; $environment->standalone_postgresql_id = $this->resource->id;
break; break;
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'service': case 'service':
$environment->service_id = $this->resource->id; $environment->service_id = $this->resource->id;
break; break;

View File

@@ -6,12 +6,13 @@ use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component; use Livewire\Component;
class Logs extends Component class Logs extends Component
{ {
public ?string $type = null; public ?string $type = null;
public Application|StandalonePostgresql|Service $resource; public Application|StandalonePostgresql|Service|StandaloneRedis $resource;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;
public $parameters; public $parameters;
@@ -33,7 +34,14 @@ class Logs extends Component
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';
$this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail(); $resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
$this->resource = $resource;
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid; $this->container = $this->resource->uuid;

View File

@@ -30,11 +30,11 @@ class Form extends Component
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'server.name' => 'name', 'server.name' => 'Name',
'server.description' => 'description', 'server.description' => 'Description',
'server.ip' => 'ip', 'server.ip' => 'IP address',
'server.user' => 'user', 'server.user' => 'User',
'server.port' => 'port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable', 'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm' 'server.settings.is_part_of_swarm' => 'is part of swarm'
@@ -45,7 +45,8 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function serverRefresh() { public function serverRefresh()
{
$this->validateServer(); $this->validateServer();
} }
public function instantSave() public function instantSave()
@@ -61,7 +62,19 @@ class Form extends Component
$activity = InstallDocker::run($this->server); $activity = InstallDocker::run($this->server);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection();
if ($uptime) {
$this->emit('success', 'Server is reachable.');
$this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
}
public function validateServer($install = true) public function validateServer($install = true)
{ {
try { try {
@@ -69,7 +82,7 @@ class Form extends Component
if ($uptime) { if ($uptime) {
$install && $this->emit('success', 'Server is reachable.'); $install && $this->emit('success', 'Server is reachable.');
} else { } else {
$install &&$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.'); $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return; return;
} }
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();
@@ -109,7 +122,14 @@ class Form extends Component
} }
public function submit() public function submit()
{ {
$this->validate(); if(isCloud() && !isDev()) {
$this->validate();
$this->validate([
'server.ip' => 'required|ip',
]);
} else {
$this->validate();
}
$uniqueIPs = Server::all()->reject(function (Server $server) { $uniqueIPs = Server::all()->reject(function (Server $server) {
return $server->id === $this->server->id; return $server->id === $this->server->id;
})->pluck('ip')->toArray(); })->pluck('ip')->toArray();
@@ -117,6 +137,7 @@ class Form extends Component
$this->emit('error', 'IP address is already in use by another team.'); $this->emit('error', 'IP address is already in use by another team.');
return; return;
} }
refresh_server_connection($this->server->privateKey);
$this->server->settings->wildcard_domain = $this->wildcard_domain; $this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage; $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save(); $this->server->settings->save();

View File

@@ -11,13 +11,13 @@ class ByIp extends Component
{ {
public $private_keys; public $private_keys;
public $limit_reached; public $limit_reached;
public int|null $private_key_id = null; public ?int $private_key_id = null;
public $new_private_key_name; public $new_private_key_name;
public $new_private_key_description; public $new_private_key_description;
public $new_private_key_value; public $new_private_key_value;
public string $name; public string $name;
public string|null $description = null; public ?string $description = null;
public string $ip; public string $ip;
public string $user = 'root'; public string $user = 'root';
public int $port = 22; public int $port = 22;
@@ -26,16 +26,16 @@ class ByIp extends Component
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'description' => 'nullable|string', 'description' => 'nullable|string',
'ip' => 'required', 'ip' => 'required|ip',
'user' => 'required|string', 'user' => 'required|string',
'port' => 'required|integer', 'port' => 'required|integer',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'name', 'name' => 'Name',
'description' => 'description', 'description' => 'Description',
'ip' => 'ip', 'ip' => 'IP Address',
'user' => 'user', 'user' => 'User',
'port' => 'port', 'port' => 'Port',
]; ];
public function mount() public function mount()

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -11,18 +12,40 @@ class Deploy extends Component
public Server $server; public Server $server;
public bool $traefikDashboardAvailable = false; public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null; public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated']; public ?string $serverIp = null;
public function mount() { protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"];
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName(); $this->currentRoute = request()->route()->getName();
} }
public function traefikDashboardAvailable(bool $data) { public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data; $this->traefikDashboardAvailable = $data;
} }
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->server->refresh();
} }
public function ip()
{
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->emit('startProxyPolling');
$this->emit('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy() public function startProxy()
{ {
try { try {

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Logs extends Component
{
public ?Server $server = null;
public $parameters = [];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.logs');
}
}

View File

@@ -11,8 +11,6 @@ class Modal extends Component
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->proxy->set('status', 'running');
$this->server->save();
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
} }

View File

@@ -9,6 +9,11 @@ class Show extends Component
{ {
public ?Server $server = null; public ?Server $server = null;
public $parameters = []; public $parameters = [];
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -9,12 +10,42 @@ use Livewire\Component;
class Status extends Component class Status extends Component
{ {
public Server $server; public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function startProxyPolling()
{
$this->polling = true;
}
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->server->refresh();
} }
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->emit('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
CheckProxy::run($this->server, true);
$this->emit('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->emit('success', 'Proxy is running.');
} else {
$notification && $this->emit('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus() public function getProxyStatus()
{ {
try { try {
@@ -24,11 +55,4 @@ class Status extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function getProxyStatusWithNoti()
{
if ($this->server->isFunctional()) {
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
}
}
} }

View File

@@ -78,10 +78,10 @@ class Backup extends Component
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
public function submit() public function submit()
{ {
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully.');
} }
} }

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Livewire\Subscription;
use App\Models\InstanceSettings;
use Livewire\Component;
class Show extends Component
{
public InstanceSettings $settings;
public bool $alreadySubscribed = false;
public function mount() {
if (!isCloud()) {
return redirect('/');
}
$this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
public function stripeCustomerPortal() {
$session = getStripeCustomerPortalSession(currentTeam());
if (is_null($session)) {
return;
}
return redirect($session->url);
}
public function render()
{
return view('livewire.subscription.show')->layout('layouts.subscription');
}
}

View File

@@ -37,7 +37,7 @@ class Upgrade extends Component
return; return;
} }
$this->showProgress = true; $this->showProgress = true;
resolve(UpdateCoolify::class)(true); UpdateCoolify::run(true);
$this->emit('success', "Upgrading to {$this->latestVersion} version..."); $this->emit('success', "Upgrading to {$this->latestVersion} version...");
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -12,6 +12,9 @@ class DecideWhatToDoWithUser
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}
return $next($request); return $next($request);
} }
if (!auth()->user()->hasVerifiedEmail()) { if (!auth()->user()->hasVerifiedEmail()) {

View File

@@ -54,7 +54,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ApplicationPreview|null $preview = null; private ApplicationPreview|null $preview = null;
private string $container_name; private string $container_name;
private string|null $currently_running_container_name = null; private ?string $currently_running_container_name = null;
private string $basedir; private string $basedir;
private string $workdir; private string $workdir;
private ?string $build_pack = null; private ?string $build_pack = null;
@@ -67,14 +67,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose; private $docker_compose;
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
private string $dockerConfigFileExists = 'NOK';
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); // ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue; $this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
@@ -92,13 +96,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server; $this->server = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}"; $this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
ray($this->basedir, $this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server); savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
@@ -138,24 +141,51 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
try { try {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage(); $this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') { } else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile(); $this->deploy_dockerfile_buildpack();
} else { } else {
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
} else { } else {
$this->deploy(); $this->deploy_nixpacks_buildpack();
} }
} }
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
ray($e); ray($e);
$this->fail($e); $this->fail($e);
@@ -184,41 +214,42 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
private function deploy_docker_compose() // private function deploy_docker_compose()
{ // {
$dockercompose_base64 = base64_encode($this->application->dockercompose); // $dockercompose_base64 = base64_encode($this->application->dockercompose);
$this->execute_remote_command( // $this->execute_remote_command(
[ // [
"echo 'Starting deployment of {$this->application->name}.'" // "echo 'Starting deployment of {$this->application->name}.'"
], // ],
); // );
$this->prepare_builder_image(); // $this->prepare_builder_image();
$this->execute_remote_command( // $this->execute_remote_command(
[ // [
executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
], // ],
); // );
$this->build_image_name = Str::lower("{$this->application->git_repository}:build"); // $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); // $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->save_environment_variables(); // $this->save_environment_variables();
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() > 0) { // ray($containers);
foreach ($containers as $container) { // if ($containers->count() > 0) {
$containerName = data_get($container, 'Names'); // foreach ($containers as $container) {
if ($containerName) { // $containerName = data_get($container, 'Names');
instant_remote_process( // if ($containerName) {
["docker rm -f {$containerName}"], // instant_remote_process(
$this->application->destination->server // ["docker rm -f {$containerName}"],
); // $this->application->destination->server
} // );
} // }
} // }
// }
$this->execute_remote_command( // $this->execute_remote_command(
["echo -n 'Starting services (could take a while)...'"], // ["echo -n 'Starting services (could take a while)...'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
); // );
} // }
private function save_environment_variables() private function save_environment_variables()
{ {
$envs = collect([]); $envs = collect([]);
@@ -256,7 +287,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerimage() private function deploy_dockerimage_buildpack()
{ {
$this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
@@ -272,7 +303,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerfile() private function deploy_dockerfile_buildpack()
{ {
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
@@ -297,10 +328,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
// $this->build_image(); $this->build_image();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy() private function deploy_nixpacks_buildpack()
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -323,19 +354,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->execute_remote_command([ $this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]); ]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'" "echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
]); ]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
}
} }
$this->cleanup_git(); $this->cleanup_git();
if ($this->application->build_pack === 'nixpacks') { $this->generate_nixpacks_confs();
$this->generate_nixpacks_confs();
}
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@@ -429,7 +463,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->stop_running_container(); $this->stop_running_container();
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting preview deployment.'"], ["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
); );
} }
@@ -437,7 +471,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$pull = "--pull=always"; $pull = "--pull=always";
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -511,8 +549,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
if ($this->application->deploymentType() === 'deploy_key') { if ($this->application->deploymentType() === 'deploy_key') {
$port = 22;
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$port = $matches[0];
}
$private_key = base64_encode($this->application->private_key->private_key); $private_key = base64_encode($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->basedir}"; $git_clone_command = "GIT_SSH_COMMAND=\"ssh -p $port -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([ $commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -612,6 +655,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray();
}
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@@ -620,7 +667,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name, 'container_name' => $this->container_name,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview), 'labels' => $labels,
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
@@ -664,12 +711,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if ($this->build_pack === 'dockerfile') { // if ($this->build_pack === 'dockerfile') {
$docker_compose['services'][$this->container_name]['build'] = [ // $docker_compose['services'][$this->container_name]['build'] = [
'context' => $this->workdir, // 'context' => $this->workdir,
'dockerfile' => $this->workdir . $this->dockerfile_location, // 'dockerfile' => $this->workdir . $this->dockerfile_location,
]; // ];
} // }
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -762,7 +809,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->settings->is_static) { if ($this->application->settings->is_static) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]); ]);
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -795,12 +842,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
], ],
[ [
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
] ]
); );
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]); ]);
} }
} }
@@ -826,7 +873,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} }
@@ -849,7 +896,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function add_build_env_variables_to_dockerfile() private function add_build_env_variables_to_dockerfile()
{ {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/Dockerfile"), "hidden" => true, "save" => 'dockerfile' executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]); ]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
@@ -858,7 +905,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile"), executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"),
"hidden" => true "hidden" => true
]); ]);
} }

View File

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
@@ -27,11 +28,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public $timeout = 120; public $timeout = 120;
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
@@ -41,10 +37,18 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
return $this->server->uuid; return $this->server->uuid;
} }
public function __construct(public Server $server)
{
if (isDev()) {
$this->handle();
}
}
public function handle() public function handle()
{ {
try { try {
// ray("checking server status for {$this->server->name}"); // ray("checking server status for {$this->server->id}");
// ray()->clearAll(); // ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count; $serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3; $serverUptimeCheckNumberMax = 3;
@@ -53,12 +57,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) { if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...'); ray('Server unreachable, sending notification...');
// $this->server->team->notify(new Unreachable($this->server)); $this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]); $this->server->update(['unreachable_email_sent' => true]);
} }
$this->server->settings()->update([ $this->server->settings()->update([
'is_reachable' => false, 'is_reachable' => false,
]); ]);
$this->server->update([
'unreachable_count' => 0,
]);
return; return;
} }
$result = $this->server->validateConnection(); $result = $this->server->validateConnection();
@@ -82,7 +89,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->server, 'unreachable_email_sent') === true) { if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...'); ray('Server is reachable again, sending notification...');
// $this->server->team->notify(new Revived($this->server)); $this->server->team->notify(new Revived($this->server));
$this->server->update(['unreachable_email_sent' => false]); $this->server->update(['unreachable_email_sent' => false]);
} }
if ( if (
@@ -111,11 +118,18 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return data_get($value, 'Name') === '/coolify-proxy'; return data_get($value, 'Name') === '/coolify-proxy';
})->first(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
ray('Proxy not found, starting it...'); try {
if ($this->server->isProxyShouldRun()) { $shouldStart = CheckProxy::run($this->server);
StartProxy::run($this->server, false); if ($shouldStart) {
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
} }
} else { } else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save(); $this->server->save();

View File

@@ -66,50 +66,77 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('database not running'); ray('database not running');
return; return;
} }
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
$databasesToBackup = [$this->database->postgres_db];
} else {
return;
}
} else {
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
}
$this->container_name = $this->database->uuid; $this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->container_name = "coolify-db"; $this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
$this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup"; foreach ($databasesToBackup as $database) {
$this->backup_location = $this->backup_dir . $this->backup_file; $size = 0;
ray('Backing up ' . $database);
$this->backup_log = ScheduledDatabaseBackupExecution::create([ try {
'filename' => $this->backup_location, $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
'scheduled_database_backup_id' => $this->backup->id, $this->backup_location = $this->backup_dir . $this->backup_file;
]); $this->backup_log = ScheduledDatabaseBackupExecution::create([
if ($this->database->type() === 'standalone-postgresql') { 'database_name' => $database,
$this->backup_standalone_postgresql(); 'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($databaseType === 'standalone-postgresql') {
$this->backup_standalone_postgresql($database);
}
$size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
'size' => $size,
]);
} catch (\Throwable $e) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
}
} }
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_status = 'success';
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->backup_status = 'failed';
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e; throw $e;
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
} }
} }
private function backup_standalone_postgresql(): void private function backup_standalone_postgresql(string $database): void
{ {
try { try {
ray($this->backup_dir); ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -119,6 +146,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
} }
} }
@@ -131,9 +159,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
private function calculate_size(): void private function calculate_size()
{ {
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server); return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
} }
private function remove_old_backups(): void private function remove_old_backups(): void
@@ -180,13 +208,4 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
instant_remote_process([$command], $this->server); instant_remote_process([$command], $this->server);
} }
} }
private function save_backup_logs(): void
{
$this->backup_log->update([
'status' => $this->backup_status,
'message' => $this->backup_output,
'size' => $this->size,
]);
}
} }

View File

@@ -47,20 +47,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return; return;
} }
if (isDev()) { $this->dockerRootFilesystem = "/";
$this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$this->server,
false
);
}
if (!$this->dockerRootFilesystem) {
return;
}
$this->usageBefore = $this->getFilesystemUsage(); $this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange'); ray('Cleaning up ' . $this->server->name)->color('orange');

View File

@@ -23,6 +23,6 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncr
public function handle(): void public function handle(): void
{ {
resolve(UpdateCoolify::class)($this->force); UpdateCoolify::run($this->force);
} }
} }

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis $resource)
{
}
public function handle()
{
try {
$server = $this->resource->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
break;
case 'standalone-postgresql':
StopDatabase::run($this->resource);
break;
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'service':
StopService::run($this->resource);
break;
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e;
} finally {
$this->resource->delete();
}
}
}

View File

@@ -32,16 +32,8 @@ class Application extends BaseModel
]); ]);
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
// Stop Container
if ($application->destination->server->isFunctional()) {
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
}
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false); instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
} }
@@ -285,4 +277,31 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function isConfigurationChanged($save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
} }

View File

@@ -14,7 +14,7 @@ class Environment extends Model
public function can_delete_environment() public function can_delete_environment()
{ {
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0; return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
} }
public function applications() public function applications()
@@ -26,10 +26,16 @@ class Environment extends Model
{ {
return $this->hasMany(StandalonePostgresql::class); return $this->hasMany(StandalonePostgresql::class);
} }
public function redis()
{
return $this->hasMany(StandaloneRedis::class);
}
public function databases() public function databases()
{ {
return $this->postgresqls(); $postgresqls = $this->postgresqls;
$redis = $this->redis;
return $postgresqls->concat($redis);
} }
public function project() public function project()

View File

@@ -52,4 +52,8 @@ class Project extends BaseModel
{ {
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
} }
public function redis()
{
return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
}
} }

View File

@@ -93,8 +93,11 @@ class Server extends BaseModel
public function proxyType() public function proxyType()
{ {
$type = $this->proxy->get('type'); $proxyType = $this->proxy->get('type');
if (is_null($type)) { if ($proxyType === ProxyTypes::NONE->value) {
return $proxyType;
}
if (is_null($proxyType)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value; $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value; $this->proxy->status = ProxyStatus::EXITED->value;
$this->save(); $this->save();
@@ -120,7 +123,8 @@ class Server extends BaseModel
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls; $postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]); $redis = $standaloneDocker->redis;
return $postgresqls->concat($redis);
})->flatten(); })->flatten();
} }
public function applications() public function applications()
@@ -189,23 +193,24 @@ class Server extends BaseModel
} }
public function isProxyShouldRun() public function isProxyShouldRun()
{ {
$shouldRun = false;
if ($this->proxyType() === ProxyTypes::NONE->value) { if ($this->proxyType() === ProxyTypes::NONE->value) {
return false; return false;
} }
foreach ($this->applications() as $application) { // foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) { // if (data_get($application, 'fqdn')) {
$shouldRun = true; // $shouldRun = true;
break; // break;
} // }
} // }
if ($this->id === 0) { // ray($this->services()->get());
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) { // if ($this->id === 0) {
$shouldRun = true; // $settings = InstanceSettings::get();
} // if (data_get($settings, 'fqdn')) {
} // $shouldRun = true;
return $shouldRun; // }
// }
return true;
} }
public function isFunctional() public function isFunctional()
{ {

View File

@@ -19,7 +19,6 @@ class Service extends BaseModel
static::deleting(function ($service) { static::deleting(function ($service) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -27,7 +26,6 @@ class Service extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
} }
foreach ($service->databases()->get() as $database) { foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get(); $storages = $database->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -384,7 +382,7 @@ class Service extends BaseModel
$value = Str::of($variable); $value = Str::of($variable);
} }
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew) { if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}"); $fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) { if (substr_count($key->value(), '_') === 3) {
@@ -540,7 +538,7 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
@@ -570,7 +568,7 @@ class Service extends BaseModel
'networks' => $topLevelNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
]; ];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2); $this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2); $this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save(); $this->save();
$this->saveComposeConfigs(); $this->saveComposeConfigs();
return collect([]); return collect([]);

View File

@@ -15,6 +15,10 @@ class StandaloneDocker extends BaseModel
{ {
return $this->morphMany(StandalonePostgresql::class, 'destination'); return $this->morphMany(StandalonePostgresql::class, 'destination');
} }
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function server() public function server()
{ {

View File

@@ -29,21 +29,13 @@ class StandalonePostgresql extends BaseModel
]); ]);
}); });
static::deleting(function ($database) { static::deleting(function ($database) {
// Stop Container $storages = $database->persistentStorages()->get();
instant_remote_process( foreach ($storages as $storage) {
["docker rm -f {$database->uuid}"], instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
} }
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
}); });
} }

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneRedis extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'redis-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
// Normal Deployments
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-redis';
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -72,7 +72,7 @@ class User extends Authenticatable implements SendsEmail
$mail->view('emails.email-verification', [ $mail->view('emails.email-verification', [
'url' => $url, 'url' => $url,
]); ]);
$mail->subject('Coolify Cloud: Verify your email.'); $mail->subject('Coolify: Verify your email.');
send_user_an_email($mail, $this->email); send_user_an_email($mail, $this->email);
} }
public function sendPasswordResetNotification($token): void public function sendPasswordResetNotification($token): void
@@ -118,6 +118,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam() public function currentTeam()
{ {
return Cache::remember('team:' . auth()->user()->id, 3600, function () { return Cache::remember('team:' . auth()->user()->id, 3600, function () {
if (is_null(data_get(session('currentTeam'), 'id'))) {
return auth()->user()->teams[0];
}
return Team::find(session('currentTeam')->id); return Team::find(session('currentTeam')->id);
}); });
} }

View File

@@ -15,25 +15,23 @@ class StatusChanged extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public Application $application; public string $resource_name;
public string $application_name;
public string $project_uuid; public string $project_uuid;
public string $environment_name; public string $environment_name;
public ?string $application_url = null; public ?string $resource_url = null;
public ?string $fqdn; public ?string $fqdn;
public function __construct($application) public function __construct(public Application $resource)
{ {
$this->application = $application; $this->resource_name = data_get($resource, 'name');
$this->application_name = data_get($application, 'name'); $this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name');
$this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($resource, 'fqdn', null);
$this->fqdn = data_get($application, 'fqdn', null);
if (Str::of($this->fqdn)->explode(',')->count() > 1) { if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first(); $this->fqdn = Str::of($this->fqdn)->explode(',')->first();
} }
$this->application_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}"; $this->resource_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->resource->uuid}";
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@@ -45,32 +43,32 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$fqdn = $this->fqdn; $fqdn = $this->fqdn;
$mail->subject("Coolify: {$this->application_name} has been stopped"); $mail->subject("Coolify: {$this->resource_name} has been stopped");
$mail->view('emails.application-status-changes', [ $mail->view('emails.application-status-changes', [
'name' => $this->application_name, 'name' => $this->resource_name,
'fqdn' => $fqdn, 'fqdn' => $fqdn,
'application_url' => $this->application_url, 'resource_url' => $this->resource_url,
]); ]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
$message = 'Coolify: ' . $this->application_name . ' has been stopped. $message = 'Coolify: ' . $this->resource_name . ' has been stopped.
'; ';
$message .= '[Open Application in Coolify](' . $this->application_url . ')'; $message .= '[Open Application in Coolify](' . $this->resource_url . ')';
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = 'Coolify: ' . $this->application_name . ' has been stopped.'; $message = 'Coolify: ' . $this->resource_name . ' has been stopped.';
return [ return [
"message" => $message, "message" => $message,
"buttons" => [ "buttons" => [
[ [
"text" => "Open Application in Coolify", "text" => "Open Application in Coolify",
"url" => $this->application_url "url" => $this->resource_url
] ]
], ],
]; ];

View File

@@ -0,0 +1,27 @@
<?php
namespace App\View\Components\Server;
use App\Models\Server;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Sidebar extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public Server $server, public $parameters)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.server.sidebar');
}
}

View File

@@ -1,6 +1,6 @@
<?php <?php
const DATABASE_TYPES = ['postgresql']; const DATABASE_TYPES = ['postgresql','redis'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
'hourly' => '0 * * * *', 'hourly' => '0 * * * *',

View File

@@ -3,6 +3,7 @@
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
function generate_database_name(string $type): string function generate_database_name(string $type): string
@@ -27,6 +28,21 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
]); ]);
} }
function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneRedis::create([
'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
/** /**
* Delete file locally on the filesystem. * Delete file locally on the filesystem.
* @param string $filename * @param string $filename

View File

@@ -147,18 +147,19 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; return $labels;
} }
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled) function fqdnLabelsForTraefik($uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
foreach ($domains as $domain) { foreach ($domains as $domain) {
$uuid = (string)new Cuid2(7);
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
$schema = $url->getScheme(); $schema = $url->getScheme();
$port = $url->getPort(); $port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
}
$http_label = "{$uuid}-http"; $http_label = "{$uuid}-http";
$https_label = "{$uuid}-https"; $https_label = "{$uuid}-https";
@@ -205,15 +206,19 @@ function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
} }
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
{ {
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$onlyPort = null;
if (count($ports) === 1) {
$onlyPort = $ports[0];
}
$pull_request_id = data_get($preview, 'pull_request_id', 0); $pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application, $pull_request_id); // $container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id; $appId = $application->id;
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $pull_request_id; $appId = $appId . '-pr-' . $pull_request_id;
} }
$labels = collect([]); $labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id)); $labels = $labels->merge(defaultLabels($appId, $application->uuid, $pull_request_id));
if ($application->fqdn) { if ($application->fqdn) {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(','); $domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@@ -221,7 +226,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled)); $labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
} }
return $labels->all(); return $labels->all();
} }

View File

@@ -2,7 +2,6 @@
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
function get_proxy_path() function get_proxy_path()
@@ -21,7 +20,7 @@ function connectProxyToNetworks(Server $server)
} }
$commands = $networks->map(function ($network) { $commands = $networks->map(function ($network) {
return [ return [
"echo '####### Connecting coolify-proxy to $network network...'", "echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
]; ];
@@ -102,6 +101,8 @@ function generate_default_proxy_configuration(Server $server)
]; ];
if (isDev()) { if (isDev()) {
$config['services']['traefik']['command'][] = "--log.level=debug"; $config['services']['traefik']['command'][] = "--log.level=debug";
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
} }
$config = Yaml::dump($config, 4, 2); $config = Yaml::dump($config, 4, 2);
SaveConfiguration::run($server, $config); SaveConfiguration::run($server, $config);
@@ -184,81 +185,3 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
} }
} }
} }
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up --build -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -108,12 +108,13 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true)
} }
function instant_remote_process(Collection|array $command, Server $server, $throwError = true) function instant_remote_process(Collection|array $command, Server $server, $throwError = true)
{ {
$timeout = config('constants.ssh.command_timeout');
if ($command instanceof Collection) { if ($command instanceof Collection) {
$command = $command->toArray(); $command = $command->toArray();
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$ssh_command = generateSshCommand($server, $command_string); $ssh_command = generateSshCommand($server, $command_string);
$process = Process::run($ssh_command); $process = Process::timeout($timeout)->run($ssh_command);
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();
if ($exitCode !== 0) { if ($exitCode !== 0) {

View File

@@ -110,7 +110,10 @@ function getStripeCustomerPortalSession(Team $team)
{ {
Stripe::setApiKey(config('subscription.stripe_api_key')); Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.index'); $return_url = route('team.index');
$stripe_customer_id = $team->subscription->stripe_customer_id; $stripe_customer_id = data_get($team,'subscription.stripe_customer_id');
if (!$stripe_customer_id) {
return null;
}
$session = \Stripe\BillingPortal\Session::create([ $session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id, 'customer' => $stripe_customer_id,
'return_url' => $return_url, 'return_url' => $return_url,

View File

@@ -1,7 +1,7 @@
<?php <?php
return [ return [
'docs' => 'https://coolify.io/contact', 'docs' => 'https://coolify.io/docs/contact',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false), 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io', 'license_url' => 'https://licenses.coollabs.io',

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.73', 'release' => '4.0.0-beta.93',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.73'; return '4.0.0-beta.93';

View File

@@ -23,6 +23,7 @@ return new class extends Migration
$table->string('git_repository'); $table->string('git_repository');
$table->string('git_branch'); $table->string('git_branch');
$table->string('git_commit_sha')->default('HEAD'); $table->string('git_commit_sha')->default('HEAD');
// TODO: remove this column, it is not used
$table->string('git_full_url')->nullable(); $table->string('git_full_url')->nullable();
$table->string('docker_registry_image_name')->nullable(); $table->string('docker_registry_image_name')->nullable();

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_redis', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('redis_password');
$table->longText('redis_conf')->nullable();
$table->string('status')->default('exited');
$table->string('image')->default('redis:7.2');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default("0");
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_redis');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_redis_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_redis_id');
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->text('databases_to_backup')->nullable();
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->string('database_name')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('databases_to_backup');
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->dropColumn('database_name');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_labels')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('custom_labels');
});
}
};

View File

@@ -13,8 +13,8 @@ use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use DB;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -22,6 +22,12 @@ class ProductionSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
if (isCloud()) {
echo "Running in cloud mode.\n";
} else {
echo "Running in self-hosted mode.\n";
}
// Fix for 4.0.0-beta.37 // Fix for 4.0.0-beta.37
if (User::find(0) !== null && Team::find(0) !== null) { if (User::find(0) !== null && Team::find(0) !== null) {
if (DB::table('team_user')->where('user_id', 0)->first() === null) { if (DB::table('team_user')->where('user_id', 0)->first() === null) {
@@ -60,7 +66,7 @@ class ProductionSeeder extends Seeder
]); ]);
} }
if (config('app.name') !== 'Coolify Cloud') { if (!isCloud()) {
// Save SSH Keys for the Coolify Host // Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal"; $coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");

View File

@@ -16,10 +16,6 @@ class ServerSeeder extends Seeder
'ip' => "coolify-testing-host", 'ip' => "coolify-testing-host",
'team_id' => 0, 'team_id' => 0,
'private_key_id' => 0, 'private_key_id' => 0,
// 'proxy' => ServerMetadata::from([
// 'type' => ProxyTypes::TRAEFIK_V2->value,
// 'status' => ProxyStatus::EXITED->value
// ]),
]); ]);
} }
} }

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Seeders;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Database\Seeder;
class StandaloneRedisSeeder extends Seeder
{
public function run(): void
{
StandaloneRedis::create([
'name' => 'Local PostgreSQL',
'description' => 'Local PostgreSQL for testing',
'redis_password' => 'redis',
'environment_id' => 1,
'destination_id' => 0,
'destination_type' => StandaloneDocker::class,
]);
}
}

View File

@@ -34,14 +34,16 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}" POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust" POSTGRES_HOST_AUTH_METHOD: "trust"
volumes: volumes:
- coolify-pg-data-dev:/var/lib/postgresql/data - ./_data/coolify/_volumes/database/:/var/lib/postgresql/data
# - coolify-pg-data-dev:/var/lib/postgresql/data
redis: redis:
ports: ports:
- "${FORWARD_REDIS_PORT:-6379}:6379" - "${FORWARD_REDIS_PORT:-6379}:6379"
env_file: env_file:
- .env - .env
volumes: volumes:
- coolify-redis-data-dev:/data - ./_data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data
vite: vite:
image: node:19 image: node:19
working_dir: /var/www/html working_dir: /var/www/html
@@ -56,7 +58,8 @@ services:
volumes: volumes:
- /:/host - /:/host
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- coolify-data-dev:/data/coolify - ./_data/coolify/:/data/coolify
# - coolify-data-dev:/data/coolify
mailpit: mailpit:
image: "axllent/mailpit:latest" image: "axllent/mailpit:latest"
container_name: coolify-mail container_name: coolify-mail
@@ -76,7 +79,8 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}" MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}" MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes: volumes:
- coolify-minio-data-dev:/data - ./_data/coolify/_volumes/minio/:/data
# - coolify-minio-data-dev:/data
networks: networks:
- coolify - coolify

View File

@@ -27,4 +27,4 @@ RUN mkdir -p ~/.ssh
RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys
EXPOSE 22 EXPOSE 22
CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"] CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0", "-o", "Port=22"]

View File

@@ -1,2 +1,2 @@
User-agent: * User-agent: *
Disallow: Disallow: /

View File

@@ -7,14 +7,13 @@
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}" @if ($database->getMorphClass() === 'App\Models\StandalonePostgresql')
href="{{ route('project.database.backups.all', $parameters) }}"> <a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
<button>Backups</button> href="{{ route('project.database.backups.all', $parameters) }}">
</a> <button>Backups</button>
{{-- <x-applications.links :application="$application" /> --}} </a>
@endif
<div class="flex-1"></div> <div class="flex-1"></div>
{{-- <x-applications.advanced :application="$application" /> --}}
@if ($database->status !== 'exited') @if ($database->status !== 'exited')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"

View File

@@ -2,7 +2,9 @@
<livewire:server.proxy.modal :server="$server" /> <livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
<livewire:server.proxy.status :server="$server" /> @if ($server->proxyType() !== 'NONE')
<livewire:server.proxy.status :server="$server" />
@endif
</div> </div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div> <div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main"> <nav class="navbar-main">

View File

@@ -0,0 +1,20 @@
<div>
@if ($server->isFunctional())
<div class="flex h-full pr-4">
<div class="flex flex-col gap-4 min-w-fit">
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', $parameters) }}">
<button>Configuration</button>
</a>
@if (data_get($server, 'proxy.type') !== 'NONE')
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
href="{{ route('server.proxy.logs', $parameters) }}">
<button>Logs</button>
</a>
@endif
</div>
</div>
@else
<div>Server is not validated. Validate first.</div>
@endif
</div>

View File

@@ -1,11 +1,14 @@
<div class="pb-6"> <div class="pb-6">
<h1>Team</h1> <div class="flex items-end gap-2">
<h1>Team</h1>
<a href="/team/new"><x-forms.button>+ New Team</x-forms.button></a>
</div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center"> <ol class="inline-flex items-center">
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<span>Currently active team: <span <span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span> class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div> </div>
</li> </li>
</ol> </ol>

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html data-theme="coollabs" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin> <link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet"> <link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
<meta name="robots" content="noindex">
<title>Coolify</title> <title>Coolify</title>
@env('local') @env('local')
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" /> <link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />

View File

@@ -200,7 +200,7 @@
label="Description" id="remoteServerDescription" /> label="Description" id="remoteServerDescription" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required placeholder="Hostname or IP address" label="Hostname or IP Address" <x-forms.input required placeholder="127.0.0.1" label="IP Address"
id="remoteServerHost" /> id="remoteServerHost" />
<x-forms.input required placeholder="Port number of your server. Default is 22." <x-forms.input required placeholder="Port number of your server. Default is 22."
label="Port" id="remoteServerPort" /> label="Port" id="remoteServerPort" />

View File

@@ -14,29 +14,12 @@
<span>Your subscription has been activated! Welcome onboard!</span> <span>Your subscription has been activated! Welcome onboard!</span>
</div> </div>
@endif @endif
<div class="w-full rounded stats stats-vertical lg:stats-horizontal"> @if ($projects->count() === 0 && $servers->count() === 0)
<div class="stat"> No resources found. Add your first server / private key <a class="text-white underline" href="{{route('server.create')}}">here</a>.
<div class="stat-title">Servers</div> @endif
<div class="stat-value">{{ $servers->count() }} </div> @if ($projects->count() > 0)
</div> <h3 class="pb-4">Projects</h3>
@endif
<div class="stat">
<div class="stat-title">Projects</div>
<div class="stat-value">{{ $projects->count() }}</div>
</div>
<div class="stat">
<div class="stat-title">Resources</div>
<div class="stat-value">{{ $resources }}</div>
<div class="stat-desc">Applications, databases, etc...</div>
</div>
<div class="stat">
<div class="stat-title">S3 Storages</div>
<div class="stat-value">{{ $s3s }}</div>
</div>
</div>
<h3 class="pb-4">Projects</h3>
@if ($projects->count() === 1) @if ($projects->count() === 1)
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
@else @else
@@ -44,7 +27,7 @@
@endif @endif
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data <div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="gotoProject('{{ $project->uuid }}')"> x-on:click="gotoProject('{{ $project->uuid }}','{{ data_get($project, 'environments.0.name', 'production') }}')">
@if (data_get($project, 'environments.0.name')) @if (data_get($project, 'environments.0.name'))
<a class="flex flex-col flex-1 mx-6 hover:no-underline" <a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
@@ -60,6 +43,10 @@
{{ $project->description }}</div> {{ $project->description }}</div>
</a> </a>
@endif @endif
<a class="mx-4 rounded group-hover:text-white hover:no-underline "
href="{{ route('project.resources.new', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="font-bold hover:text-warning">+ New Resource</span>
</a>
<a class="mx-4 rounded group-hover:text-white" <a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
@@ -74,7 +61,9 @@
</div> </div>
@endforeach @endforeach
</div> </div>
<h3 class="py-4">Servers</h3> @if ($projects->count() > 0)
<h3 class="pb-4">Servers</h3>
@endif
@if ($servers->count() === 1) @if ($servers->count() === 1)
<div class="grid grid-cols-1 gap-2"> <div class="grid grid-cols-1 gap-2">
@else @else
@@ -108,11 +97,11 @@
</a> </a>
@endforeach @endforeach
</div> </div>
<script> <script>
function gotoProject(uuid) { function gotoProject(uuid, environment = 'production') {
window.location.href = '/project/' + uuid; window.location.href = '/project/' + uuid + '/' + environment;
} }
</script> </script>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}} {{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div> </div>

View File

@@ -7,6 +7,6 @@
@endif @endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' || @if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued') data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="cancel">Cancel deployment</x-forms.button> <x-forms.button wire:click.prevent="cancel">Cancel Deployment</x-forms.button>
@endif @endif
</div> </div>

View File

@@ -5,8 +5,12 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($isConfigurationChanged && !is_null($application->config_hash))
<div class="font-bold text-warning">Configuration not applied to the running application. You need to
redeploy.</div>
@endif
</div> </div>
<div class="">General configuration for your application.</div> <div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row"> <div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.name" label="Name" required /> <x-forms.input id="application.name" label="Name" required />
@@ -21,7 +25,7 @@
@if (!$application->dockerfile) @if (!$application->dockerfile)
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select id="application.build_pack" label="Build Pack" required> <x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option> <option value="dockerimage">Docker Image</option>
@@ -73,15 +77,14 @@
</div> </div>
@else @else
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" /> <x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" required label="Docker Image Tag" /> <x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
</div> </div>
@endif @endif
@if ($application->dockerfile) @if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static)
@@ -93,6 +96,12 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." /> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div> </div>
@if ($labelsChanged)
<x-forms.textarea label="Custom Labels" rows="15" id="customLabels"></x-forms.textarea>
@else
<x-forms.textarea label="Coolify Generated Labels" rows="15" id="customLabels"></x-forms.textarea>
@endif
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
</div> </div>
<h3>Advanced</h3> <h3>Advanced</h3>
<div class="flex flex-col"> <div class="flex flex-col">

View File

@@ -36,7 +36,7 @@
</div> </div>
@isset($application->private_key_id) @isset($application->private_key_id)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attache Private Key: <span <div class="py-2 pt-4">Currently attached Private Key: <span
class="text-warning">{{ $application->private_key->name }}</span> class="text-warning">{{ $application->private_key->name }}</span>
</div> </div>

View File

@@ -26,6 +26,7 @@
</div> </div>
@endif @endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Databases To Backup" helper="Comma separated list of databases to backup. Empty will include the default one." id="backup.databases_to_backup" />
<x-forms.input label="Frequency" id="backup.frequency" /> <x-forms.input label="Frequency" id="backup.frequency" />
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" /> <x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
</div> </div>

View File

@@ -1,18 +1,19 @@
<div class="flex flex-col flex-col-reverse gap-2"> <div class="flex flex-col-reverse gap-2">
@forelse($executions as $execution) @forelse($executions as $execution)
<form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([ <form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([
'border-green-500' => data_get($execution, 'status') === 'success', 'border-green-500' => data_get($execution, 'status') === 'success',
'border-red-500' => data_get($execution, 'status') === 'failed', 'border-red-500' => data_get($execution, 'status') === 'failed',
])> ])>
<div>Started At: {{ data_get($execution, 'created_at') }}</div> <div>Database: {{ data_get($execution, 'database_name', 'N/A') }}</div>
<div>Status: {{ data_get($execution, 'status') }}</div> <div>Status: {{ data_get($execution, 'status') }}</div>
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
@if (data_get($execution, 'message')) @if (data_get($execution, 'message'))
<div>Message: {{ data_get($execution, 'message') }}</div> <div>Message: {{ data_get($execution, 'message') }}</div>
@endif @endif
<div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }} <div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }}
kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 2) }} MB kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
</div> </div>
<div>Location: {{ data_get($execution, 'filename') }}</div> <div>Location: {{ data_get($execution, 'filename', 'N/A') }}</div>
<livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" /> <livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" />
</form> </form>
@empty @empty

View File

@@ -62,7 +62,7 @@
label="Public Port" /> label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" /> <x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div> </div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" /> <x-forms.input label="Postgres URL" type="password" readonly wire:model="db_url" />
</div> </div>
</form> </form>
<div class="pb-16"> <div class="pb-16">

View File

@@ -0,0 +1,28 @@
<div>
<form wire:submit.prevent="submit" class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<h2>General</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="database.name" />
<x-forms.input label="Description" id="database.description" />
<x-forms.input label="Image" id="database.image" required
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/redis'>https://hub.docker.com/_/redis</a>" />
</div>
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Redis URL" type="password" readonly wire:model="db_url" />
</div>
<x-forms.textarea helper="<a target='_blank' class='text-white underline' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>" label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
</form>
</div>

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