diff --git a/.env.development.example b/.env.development.example index 16e17d717..d6f55b049 100644 --- a/.env.development.example +++ b/.env.development.example @@ -7,6 +7,7 @@ USERID= GROUPID= PROJECT_PATH_ON_HOST=/Users/your-username-here/code/coollabsio/coolify SERVEO_URL= +MUX_ENABLED=false ############################################################################################################ APP_NAME=Coolify diff --git a/app/Actions/Proxy/CheckProxySettingsInSync.php b/app/Actions/Proxy/CheckProxySettingsInSync.php new file mode 100644 index 000000000..410ebcfba --- /dev/null +++ b/app/Actions/Proxy/CheckProxySettingsInSync.php @@ -0,0 +1,33 @@ +trim(); + } else { + $final_output = Str::of($output)->trim(); + } + $docker_compose_yml_base64 = base64_encode($final_output); + $server->extra_attributes->last_saved_proxy_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->save(); + if (is_null($output)) { + instantRemoteProcess([ + "mkdir -p $proxy_path", + "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", + ], $server); + } + return $final_output; + } +} diff --git a/app/Actions/Proxy/InstallProxy.php b/app/Actions/Proxy/InstallProxy.php index fb908b3d4..2cbd4103d 100644 --- a/app/Actions/Proxy/InstallProxy.php +++ b/app/Actions/Proxy/InstallProxy.php @@ -3,100 +3,65 @@ namespace App\Actions\Proxy; use App\Enums\ActivityTypes; +use App\Enums\ProxyTypes; use App\Models\Server; -use Symfony\Component\Yaml\Yaml; +use Illuminate\Support\Collection; +use Spatie\Activitylog\Models\Activity; +use Illuminate\Support\Str; class InstallProxy { - public function __invoke(Server $server) + public Collection $networks; + + public function __invoke(Server $server): Activity { - $docker_compose_yml_base64 = base64_encode( - $this->getDockerComposeContents() - ); + $proxy_path = config('coolify.proxy_config_path'); + + $networks = collect($server->standaloneDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + if ($networks->count() === 0) { + $this->networks = collect(['coolify']); + } + $create_networks_command = $this->networks->map(function ($network) { + return "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1"; + }); + + $configuration = instantRemoteProcess([ + "cat $proxy_path/docker-compose.yml", + ], $server, false); + if (is_null($configuration)) { + $configuration = Str::of(getProxyConfiguration($server))->trim(); + } else { + $configuration = Str::of($configuration)->trim(); + } + $docker_compose_yml_base64 = base64_encode($configuration); + $server->extra_attributes->last_applied_proxy_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->save(); $env_file_base64 = base64_encode( $this->getEnvContents() ); - $activity = remoteProcess([ - 'mkdir -p projects', - 'mkdir -p projects/proxy', - 'mkdir -p projects/proxy/letsencrypt', - 'cd projects/proxy', - "echo '$docker_compose_yml_base64' | base64 -d > docker-compose.yml", - "echo '$env_file_base64' | base64 -d > .env", + ...$create_networks_command, + "echo 'Docker networks created...'", + "mkdir -p $proxy_path", + "cd $proxy_path", + "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", + "echo '$env_file_base64' | base64 -d > $proxy_path/.env", + "echo 'Docker compose file created...'", + "echo 'Pulling docker image...'", + 'docker compose pull -q', + "echo 'Stopping proxy...'", + 'docker compose down -v --remove-orphans', + "echo 'Starting proxy...'", 'docker compose up -d --remove-orphans', - 'docker ps', + "echo 'Proxy installed successfully...'" ], $server, ActivityTypes::INLINE->value); return $activity; } - protected function getDockerComposeContents() - { - return Yaml::dump($this->getComposeData()); - } - - /** - * @return array - */ - protected function getComposeData(): array - { - $cwd = config('app.env') === 'local' - ? config('proxy.project_path_on_host') . '/_testing_hosts/host_2_proxy' - : '.'; - - ray($cwd); - - return [ - "version" => "3.7", - "networks" => [ - "coolify" => [ - "external" => true, - ], - ], - "services" => [ - "traefik" => [ - "container_name" => "coolify-proxy", - "image" => "traefik:v2.10", - "restart" => "always", - "extra_hosts" => [ - "host.docker.internal:host-gateway", - ], - "networks" => [ - "coolify", - ], - "ports" => [ - "80:80", - "443:443", - "8080:8080", - ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "{$cwd}/letsencrypt:/letsencrypt", - "{$cwd}/traefik.auth:/auth/traefik.auth", - ], - "command" => [ - "--api.dashboard=true", - "--api.insecure=true", - "--entrypoints.http.address=:80", - "--entrypoints.https.address=:443", - "--providers.docker=true", - "--providers.docker.exposedbydefault=false", - ], - "labels" => [ - "traefik.enable=true", - "traefik.http.routers.traefik.entrypoints=http", - 'traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)', - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", - ], - ], - ], - ]; - } - protected function getEnvContents() { $data = [ diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 04e3415d5..a636e1c5d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,6 +4,7 @@ namespace App\Console; use App\Jobs\ContainerStatusJob; use App\Jobs\DockerCleanupDanglingImagesJob; +use App\Jobs\ProxyCheckJob; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -16,6 +17,7 @@ class Kernel extends ConsoleKernel { $schedule->job(new ContainerStatusJob)->everyMinute(); $schedule->job(new DockerCleanupDanglingImagesJob)->everyMinute(); + // $schedule->job(new ProxyCheckJob)->everyMinute(); } /** diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 923006789..ed2fc38b4 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -32,7 +32,7 @@ class Form extends Component $this->emit('newMonitorActivity', $activity->id); } - public function checkServer() + public function validateServer() { try { $this->uptime = instantRemoteProcess(['uptime'], $this->server, false); diff --git a/app/Http/Livewire/Server/Proxy.php b/app/Http/Livewire/Server/Proxy.php index bc4298287..1991b75e9 100644 --- a/app/Http/Livewire/Server/Proxy.php +++ b/app/Http/Livewire/Server/Proxy.php @@ -2,8 +2,10 @@ namespace App\Http\Livewire\Server; +use App\Actions\Proxy\CheckProxySettingsInSync; use App\Actions\Proxy\InstallProxy; -use App\Enums\ActivityTypes; +use App\Enums\ProxyTypes; +use Illuminate\Support\Str; use App\Models\Server; use Livewire\Component; @@ -11,22 +13,56 @@ class Proxy extends Component { public Server $server; - protected string $selectedProxy = ''; + public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2; + public $proxy_settings = null; - public function mount(Server $server) - { - $this->server = $server; - } - - public function runInstallProxy() + public function installProxy() { + $this->saveConfiguration($this->server); $activity = resolve(InstallProxy::class)($this->server); - $this->emit('newMonitorActivity', $activity->id); } - public function render() + public function proxyStatus() { - return view('livewire.server.proxy'); + $this->server->extra_attributes->proxy_status = checkContainerStatus(server: $this->server, container_id: 'coolify-proxy'); + $this->server->save(); + } + public function setProxy() + { + $this->server->extra_attributes->proxy_type = $this->selectedProxy->value; + $this->server->extra_attributes->proxy_status = 'exited'; + $this->server->save(); + } + public function stopProxy() + { + instantRemoteProcess([ + "docker rm -f coolify-proxy", + ], $this->server); + $this->server->extra_attributes->proxy_status = 'exited'; + $this->server->save(); + } + public function saveConfiguration() + { + try { + $proxy_path = config('coolify.proxy_config_path'); + $this->proxy_settings = Str::of($this->proxy_settings)->trim(); + $docker_compose_yml_base64 = base64_encode($this->proxy_settings); + $this->server->extra_attributes->last_saved_proxy_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $this->server->save(); + instantRemoteProcess([ + "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", + ], $this->server); + } catch (\Exception $e) { + return generalErrorHandler($e); + } + } + public function checkProxySettingsInSync() + { + try { + $this->proxy_settings = resolve(CheckProxySettingsInSync::class)($this->server); + } catch (\Exception $e) { + return generalErrorHandler($e); + } } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index bc05d6e0b..de3660de4 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -67,9 +67,7 @@ class ContainerStatusJob implements ShouldQueue return; } if ($application->destination->server) { - $container = instantRemoteProcess(["docker inspect --format '{{json .State}}' {$this->container_id}"], $application->destination->server); - $container = formatDockerCmdOutputToJson($container); - $application->status = $container[0]['Status']; + $application->status = checkContainerStatus(server: $application->destination->server, container_id: $this->container_id); $application->save(); } } diff --git a/app/Jobs/DeployApplicationJob.php b/app/Jobs/DeployApplicationJob.php index 0e1ad11b5..31159d2df 100644 --- a/app/Jobs/DeployApplicationJob.php +++ b/app/Jobs/DeployApplicationJob.php @@ -69,7 +69,6 @@ class DeployApplicationJob implements ShouldQueue protected function stopRunningContainer() { $this->executeNow([ - "echo -n 'Removing old instance... '", $this->execute_in_builder("docker rm -f {$this->application->uuid} >/dev/null 2>&1"), "echo 'Done.'", diff --git a/app/Jobs/ProxyCheckJob.php b/app/Jobs/ProxyCheckJob.php new file mode 100755 index 000000000..4c7ce12e6 --- /dev/null +++ b/app/Jobs/ProxyCheckJob.php @@ -0,0 +1,45 @@ +get(); + + foreach ($servers as $server) { + $status = checkContainerStatus(server: $server, container_id: $container_name); + if ($status === 'running') { + continue; + } + resolve(InstallProxy::class)($server); + } + } catch (\Throwable $th) { + //throw $th; + } + } +} diff --git a/bootstrap/helpers.php b/bootstrap/helpers.php index 546ef6604..085604cd5 100644 --- a/bootstrap/helpers.php +++ b/bootstrap/helpers.php @@ -40,7 +40,7 @@ if (!function_exists('generalErrorHandler')) { 'error' => $error->getMessage(), ]); } else { - dump($error); + // dump($error); } } } @@ -106,7 +106,7 @@ if (!function_exists('generateSshCommand')) { $delimiter = 'EOF-COOLIFY-SSH'; Storage::disk('local')->makeDirectory('.ssh'); $ssh_command = "ssh "; - if ($isMux) { + if ($isMux && config('coolify.mux_enabled')) { $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/.ssh/ssh_mux_%h_%p_%r '; } $ssh_command .= "-i {$private_key_location} " @@ -165,10 +165,9 @@ if (!function_exists('instantRemoteProcess')) { $exitCode = $process->exitCode(); if ($exitCode !== 0) { if (!$throwError) { - return false; + return null; } - Log::error($process->errorOutput()); - throw new \RuntimeException('There was an error running the command.'); + throw new \RuntimeException($process->errorOutput()); } return $output; } @@ -196,6 +195,7 @@ use Lcobucci\JWT\Encoding\JoseEncoder; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token\Builder; +use Symfony\Component\Yaml\Yaml; if (!function_exists('generate_github_installation_token')) { function generate_github_installation_token(GithubApp $source) @@ -244,3 +244,73 @@ if (!function_exists('getParameters')) { return Route::current()->parameters(); } } +if (!function_exists('checkContainerStatus')) { + function checkContainerStatus(Server $server, string $container_id, bool $throwError = false) + { + $container = instantRemoteProcess(["docker inspect --format '{{json .State}}' {$container_id}"], $server, $throwError); + if (!$container) { + return 'exited'; + } + $container = formatDockerCmdOutputToJson($container); + return $container[0]['Status']; + } +} +if (!function_exists('getProxyConfiguration')) { + function getProxyConfiguration(Server $server) + { + $proxy_config_path = config('coolify.proxy_config_path'); + $networks = collect($server->standaloneDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + if ($networks->count() === 0) { + $networks = collect(['coolify']); + } + $array_of_networks = collect([]); + $networks->map(function ($network) use ($array_of_networks) { + $array_of_networks[$network] = [ + "external" => true, + ]; + }); + return Yaml::dump([ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "traefik" => [ + "container_name" => "coolify-proxy", # Do not modify this! You will break everything! + "image" => "traefik:v2.10", + "restart" => "always", + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "networks" => $networks->toArray(), # Do not modify this! You will break everything! + "ports" => [ + "80:80", + "443:443", + "8080:8080", + ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_config_path}/letsencrypt:/letsencrypt", # Do not modify this! You will break everything! + "{$proxy_config_path}/traefik.auth:/auth/traefik.auth", # Do not modify this! You will break everything! + ], + "command" => [ + "--api.dashboard=true", + "--api.insecure=true", + "--entrypoints.http.address=:80", + "--entrypoints.https.address=:443", + "--providers.docker=true", + "--providers.docker.exposedbydefault=false", + ], + "labels" => [ + "traefik.enable=true", # Do not modify this! You will break everything! + "traefik.http.routers.traefik.entrypoints=http", + 'traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)', + "traefik.http.routers.traefik.service=api@internal", + "traefik.http.services.traefik.loadbalancer.server.port=8080", + "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", + ], + ], + ], + ], 4, 2); + } +} diff --git a/config/coolify.php b/config/coolify.php index aef26cdb3..0afb921dc 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -1,5 +1,8 @@ env('MUX_ENABLED', true), 'dev_webhook' => env('SERVEO_URL'), + 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), + 'proxy_config_path' => env('BASE_CONFIG_PATH', '/data/coolify') . "/proxy", ]; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index bcea84941..9cd655181 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -2,7 +2,6 @@ version: '3.8' x-testing-host: &testing-host-base - image: coolify-testing-host build: dockerfile: Dockerfile context: ./docker/testing-host diff --git a/docker/dev-ssu/Dockerfile b/docker/dev-ssu/Dockerfile index c90e53d08..e8724828e 100644 --- a/docker/dev-ssu/Dockerfile +++ b/docker/dev-ssu/Dockerfile @@ -5,4 +5,9 @@ RUN apt-get -y autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* +RUN echo "alias ll='ls -al'" >> /etc/bash.bashrc +RUN echo "alias a='php artisan'" >> /etc/bash.bashrc +RUN echo "alias mfs='php artisan migrate:fresh --seed'" >> /etc/bash.bashrc +RUN echo "alias cda='composer dump-autoload'" >> /etc/bash.bashrc + # COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/ diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index 8125614f0..1e8628d87 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -1,14 +1,16 @@ FROM ubuntu:22.04 ARG TARGETPLATFORM - # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=20.10.18 +ARG DOCKER_VERSION=23.0.5 # https://github.com/docker/compose/releases -# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. -ARG DOCKER_COMPOSE_VERSION=2.6.1 +ARG DOCKER_COMPOSE_VERSION=2.17.3 +# https://github.com/docker/buildx/releases +ARG DOCKER_BUILDX_VERSION=0.10.4 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=v0.27.0 +ARG PACK_VERSION=0.29.0 +# https://github.com/railwayapp/nixpacks/releases +ARG NIXPACKS_VERSION=1.6.1 ENV DEBIAN_FRONTEND noninteractive ENV TZ=UTC @@ -17,7 +19,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN apt-get update \ && apt-get install -y gnupg gosu curl ca-certificates zip unzip git git-lfs supervisor \ - sqlite3 libcap2-bin libpng-dev python2 dnsutils openssh-server sudo \ + sqlite3 libcap2-bin libpng-dev python2 dnsutils openssh-server sudo \ && apt-get -y autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -26,13 +28,14 @@ RUN apt-get update \ RUN ssh-keygen -A RUN mkdir -p /run/sshd -# Install Docker CLI, Docker Compose, and Pack -RUN mkdir -p ~/.docker/cli-plugins -RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker -RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose -RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack -RUN curl -sSL https://nixpacks.com/install.sh | bash -RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack +RUN if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ + curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx && \ + curl -sSL https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose && \ + (curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz | tar -C /usr/bin/ --no-same-owner -xzv --strip-components=1 docker/docker) && \ + (curl -sSL https://github.com/buildpacks/pack/releases/download/v${PACK_VERSION}/pack-v${PACK_VERSION}-linux.tgz | tar -C /usr/local/bin/ --no-same-owner -xzv pack) && \ + curl -sSL https://nixpacks.com/install.sh | bash && \ + chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \ + ;fi RUN groupadd docker # Setup coolify user diff --git a/resources/css/app.css b/resources/css/app.css index 7781ba274..4e1e62904 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,8 +6,9 @@ body { @apply bg-coolgray-100 text-white font-sans; } -input, textarea { - @apply border-none p-2 bg-coolgray-200 text-white disabled:text-neutral-600 read-only:text-neutral-600 read-only:select-none outline-none ; +input, +textarea { + @apply border-none p-2 bg-coolgray-200 text-white disabled:text-neutral-600 read-only:text-neutral-600 read-only:select-none outline-none; } select { @apply border-none p-2 bg-coolgray-200 text-white disabled:text-neutral-600 read-only:select-none outline-none; @@ -20,11 +21,11 @@ button { @apply relative float-left; } .main-menu:after { - content: '/'; + content: "/"; @apply absolute right-0 top-0 text-neutral-400 px-2 pt-[0.3rem]; } .magic-input { - @apply w-[25rem] rounded shadow outline-none focus:bg-neutral-700 text-white; + @apply w-[25rem] rounded outline-none bg-coolgray-400 focus:bg-neutral-700 text-white; } .magic-items { @apply absolute top-14 w-[25rem] bg-coolgray-200 border-b-2 border-r-2 border-l-2 border-solid border-coolgray-100 rounded-b; @@ -41,6 +42,9 @@ h1 { h2 { @apply text-xl font-bold py-4; } +h3 { + @apply text-lg font-bold py-4; +} .box { @apply flex items-center justify-center text-sm rounded cursor-pointer h-14 bg-coolgray-200 hover:bg-coollabs p-2; } diff --git a/resources/views/components/inputs/button.blade.php b/resources/views/components/inputs/button.blade.php index f1b42b1e3..93dcace54 100644 --- a/resources/views/components/inputs/button.blade.php +++ b/resources/views/components/inputs/button.blade.php @@ -1,17 +1,21 @@ @props([ 'isWarning' => null, + 'isBold' => false, 'disabled' => null, - 'defaultClass' => 'text-white hover:bg-coollabs h-8 rounded transition-colors', - 'defaultWarningClass' => 'text-white bg-red-500 hover:bg-red-600 h-8 rounded', - 'disabledClass' => 'text-coolgray-200 h-8 rounded', - 'loadingClass' => 'text-black bg-green-500 h-8 rounded', + 'defaultClass' => 'text-white hover:bg-coollabs h-10 rounded transition-colors', + 'defaultWarningClass' => 'text-white bg-red-500 hover:bg-red-600 h-10 rounded', + 'disabledClass' => 'text-neutral-400 h-10 rounded', + 'loadingClass' => 'text-black bg-green-500 h-10 rounded', 'confirm' => null, 'confirmAction' => null, ])