diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index 7a1cb04ed..208a6863a 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -11,7 +11,12 @@ class CheckConfiguration use AsAction; public function handle(Server $server, bool $reset = false) { - $proxy_path = get_proxy_path(); + $proxyType = $server->proxyType(); + if ($proxyType === 'NONE') { + return 'OK'; + } + $proxy_path = $server->proxyPath(); + $proxy_configuration = instant_remote_process([ "mkdir -p $proxy_path", "cat $proxy_path/docker-compose.yml", diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index ccefa8681..41f961b59 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -10,6 +10,9 @@ class CheckProxy use AsAction; public function handle(Server $server, $fromUI = false) { + if ($server->proxyType() === 'NONE') { + return false; + } if (!$server->isProxyShouldRun()) { if ($fromUI) { throw new \Exception("Proxy should not run. You selected the Custom Proxy."); diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index edf4f3434..5fb983d1a 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -15,7 +15,7 @@ class SaveConfiguration if (is_null($proxy_settings)) { $proxy_settings = CheckConfiguration::run($server, true); } - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); $docker_compose_yml_base64 = base64_encode($proxy_settings); $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index e106c1801..46ac816b4 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -15,11 +15,11 @@ class StartProxy { try { $proxyType = $server->proxyType(); - if ($proxyType === 'NONE') { + if (is_null($proxyType) || $proxyType === 'NONE') { return 'OK'; } $commands = collect([]); - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); $configuration = CheckConfiguration::run($server); if (!$configuration) { throw new \Exception("Configuration is not synced"); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index e80042573..52440dde7 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -126,6 +126,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== } public function getProxyType() { + // Set Default Proxy Type $this->selectProxy(ProxyTypes::TRAEFIK_V2->value); // $proxyTypeSet = $this->createdServer->proxy->type; // if (!$proxyTypeSet) { diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 274e198a8..92b452a42 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -124,7 +124,7 @@ class General extends Component } $this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->customLabels = $this->application->parseContainerLabels(); - if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); @@ -224,7 +224,7 @@ class General extends Component public function submit($showToaster = true) { try { - if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index e2037991a..62562179a 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -45,7 +45,7 @@ class ResourceOperations extends Component 'destination_id' => $new_destination->id, ]); $new_resource->save(); - if ($new_resource->destination->server->proxyType() === 'TRAEFIK_V2') { + if ($new_resource->destination->server->proxyType() !== 'NONE') { $customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n"); $new_resource->custom_labels = base64_encode($customLabels); $new_resource->save(); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 9799443c7..df3fae20f 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -89,6 +89,7 @@ class ByIp extends Component 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, 'proxy' => [ + // set default proxy type to traefik v2 "type" => ProxyTypes::TRAEFIK_V2->value, "status" => ProxyStatus::EXITED->value, ], diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php index ee46a3fff..aee61f7de 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php @@ -14,7 +14,7 @@ class DynamicConfigurationNavbar extends Component public function delete(string $fileName) { $server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); $file = str_replace('|', '.', $fileName); instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server); $this->dispatch('success', 'File deleted.'); diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 6e52f9d4a..b9e89321c 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -17,7 +17,7 @@ class DynamicConfigurations extends Component ]; public function loadDynamicConfigurations() { - $proxy_path = get_proxy_path(); + $proxy_path = $this->server->proxyPath(); $files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server); $files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file)); $files = $files->map(fn ($file) => trim($file)); diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index c9ceb41ee..302e603b3 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -46,7 +46,7 @@ class NewDynamicConfiguration extends Component $this->dispatch('error', 'File name is reserved.'); return; } - $proxy_path = get_proxy_path(); + $proxy_path = $this->proxyPath(); $file = "{$proxy_path}/dynamic/{$this->fileName}"; if ($this->newFile) { $exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server); diff --git a/app/Models/Server.php b/app/Models/Server.php index 4028109e2..bc65cbdba 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -118,17 +118,30 @@ class Server extends BaseModel } } } + public function proxyPath() { + $base_path = config('coolify.base_config_path'); + $proxyType = $this->proxyType(); + $proxy_path = "$base_path/proxy"; + if ($proxyType === ProxyTypes::TRAEFIK_V2->value) { + $proxy_path = $proxy_path; + } else if ($proxyType === ProxyTypes::CADDY->value) { + $proxy_path = $proxy_path . '/caddy'; + } else if ($proxyType === ProxyTypes::NGINX->value) { + $proxy_path = $proxy_path . '/nginx'; + } + return $proxy_path; + } public function proxyType() { $proxyType = $this->proxy->get('type'); if ($proxyType === ProxyTypes::NONE->value) { return $proxyType; } - if (is_null($proxyType)) { - $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; - $this->proxy->status = ProxyStatus::EXITED->value; - $this->save(); - } + // if (is_null($proxyType)) { + // $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; + // $this->proxy->status = ProxyStatus::EXITED->value; + // $this->save(); + // } return $this->proxy->get('type'); } public function scopeWithProxy(): Builder diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 5fd43daa9..53b0bbe80 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,5 +1,6 @@ $domain) { + $loop = $loop; + $url = Url::fromString($domain); + $host = $url->getHost(); + $path = $url->getPath(); + // $stripped_path = str($path)->replaceEnd('/', ''); + + $schema = $url->getScheme(); + $port = $url->getPort(); + if (is_null($port) && !is_null($onlyPort)) { + $port = $onlyPort; + } + $labels->push("caddy_{$loop}={$schema}://{$host}"); + $labels->push("caddy_{$loop}.header=-Server"); + + if ($serviceLabels) { + $labels->push("caddy_ingress_network={$uuid}"); + $labels->push("caddy_{$loop}.reverse_proxy={{upstreams}}"); + } else { + $labels->push("caddy_ingress_network={$network}"); + if ($port) { + $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}"); + } else { + $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}"); + } + $labels->push("caddy_{$loop}.handle_path={$path}*"); + } + + if ($is_gzip_enabled) { + $labels->push("caddy_{$loop}.encode=zstd gzip"); + } + if (isDev()) { + // $labels->push("caddy_{$loop}.tls=internal"); + } + } + return $labels->sort(); +} function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null) { $labels = collect([]); @@ -395,7 +436,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview } else { $domains = Str::of(data_get($application, 'fqdn'))->explode(','); } - // Add Traefik labels no matter which proxy is selected + // Add Traefik labels $labels = $labels->merge(fqdnLabelsForTraefik( uuid: $appUuid, domains: $domains, @@ -404,6 +445,16 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled() )); + // Add Caddy labels + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); } return $labels->all(); } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 1bc1bdc28..1edfaed2e 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -7,12 +7,7 @@ use App\Models\Server; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -function get_proxy_path() -{ - $base_path = config('coolify.base_config_path'); - $proxy_path = "$base_path/proxy"; - return $proxy_path; -} + function connectProxyToNetworks(Server $server) { if ($server->isSwarm()) { @@ -75,7 +70,9 @@ function connectProxyToNetworks(Server $server) } function generate_default_proxy_configuration(Server $server) { - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); + $proxy_type = $server->proxyType(); + if ($server->isSwarm()) { $networks = collect($server->swarmDockers)->map(function ($docker) { return $docker['network']; @@ -98,93 +95,129 @@ function generate_default_proxy_configuration(Server $server) "external" => true, ]; }); - $labels = [ - "traefik.enable=true", - "traefik.http.routers.traefik.entrypoints=http", - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - "coolify.managed=true", - ]; - $config = [ - "version" => "3.8", - "networks" => $array_of_networks->toArray(), - "services" => [ - "traefik" => [ - "container_name" => "coolify-proxy", - "image" => "traefik:v2.10", - "restart" => RESTART_MODE, - "extra_hosts" => [ - "host.docker.internal:host-gateway", + if ($proxy_type === 'TRAEFIK_V2') { + $labels = [ + "traefik.enable=true", + "traefik.http.routers.traefik.entrypoints=http", + "traefik.http.routers.traefik.service=api@internal", + "traefik.http.services.traefik.loadbalancer.server.port=8080", + "coolify.managed=true", + ]; + $config = [ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "traefik" => [ + "container_name" => "coolify-proxy", + "image" => "traefik:v2.10", + "restart" => RESTART_MODE, + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "networks" => $networks->toArray(), + "ports" => [ + "80:80", + "443:443", + "8080:8080", + ], + "healthcheck" => [ + "test" => "wget -qO- http://localhost:80/ping || exit 1", + "interval" => "4s", + "timeout" => "2s", + "retries" => 5, + ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_path}:/traefik", + ], + "command" => [ + "--ping=true", + "--ping.entrypoint=http", + "--api.dashboard=true", + "--api.insecure=false", + "--entrypoints.http.address=:80", + "--entrypoints.https.address=:443", + "--entrypoints.http.http.encodequerysemicolons=true", + "--entryPoints.http.http2.maxConcurrentStreams=50", + "--entrypoints.https.http.encodequerysemicolons=true", + "--entryPoints.https.http2.maxConcurrentStreams=50", + "--providers.docker.exposedbydefault=false", + "--providers.file.directory=/traefik/dynamic/", + "--providers.file.watch=true", + "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", + "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", + "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", + ], + "labels" => $labels, ], - "networks" => $networks->toArray(), - "ports" => [ - "80:80", - "443:443", - "8080:8080", - ], - "healthcheck" => [ - "test" => "wget -qO- http://localhost:80/ping || exit 1", - "interval" => "4s", - "timeout" => "2s", - "retries" => 5, - ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "{$proxy_path}:/traefik", - ], - "command" => [ - "--ping=true", - "--ping.entrypoint=http", - "--api.dashboard=true", - "--api.insecure=false", - "--entrypoints.http.address=:80", - "--entrypoints.https.address=:443", - "--entrypoints.http.http.encodequerysemicolons=true", - "--entryPoints.http.http2.maxConcurrentStreams=50", - "--entrypoints.https.http.encodequerysemicolons=true", - "--entryPoints.https.http2.maxConcurrentStreams=50", - "--providers.docker.exposedbydefault=false", - "--providers.file.directory=/traefik/dynamic/", - "--providers.file.watch=true", - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", - ], - "labels" => $labels, ], - ], - ]; - if (isDev()) { - // $config['services']['traefik']['command'][] = "--log.level=debug"; - $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; - $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; - } - if ($server->isSwarm()) { - data_forget($config, 'services.traefik.container_name'); - data_forget($config, 'services.traefik.restart'); - data_forget($config, 'services.traefik.labels'); + ]; + if (isDev()) { + // $config['services']['traefik']['command'][] = "--log.level=debug"; + $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; + $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; + } + if ($server->isSwarm()) { + data_forget($config, 'services.traefik.container_name'); + data_forget($config, 'services.traefik.restart'); + data_forget($config, 'services.traefik.labels'); - $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; - $config['services']['traefik']['deploy'] = [ - "labels" => $labels, - "placement" => [ - "constraints" => [ - "node.role==manager", + $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; + $config['services']['traefik']['deploy'] = [ + "labels" => $labels, + "placement" => [ + "constraints" => [ + "node.role==manager", + ], + ], + ]; + } else { + $config['services']['traefik']['command'][] = "--providers.docker=true"; + } + } else if ($proxy_type === 'CADDY') { + $config = [ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "caddy" => [ + "container_name" => "coolify-proxy", + "image" => "lucaslorentz/caddy-docker-proxy:2.8-alpine", + "restart" => RESTART_MODE, + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "networks" => $networks->toArray(), + "ports" => [ + "80:80", + "443:443", + ], + // "healthcheck" => [ + // "test" => "wget -qO- http://localhost:80|| exit 1", + // "interval" => "4s", + // "timeout" => "2s", + // "retries" => 5, + // ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_path}/config:/config", + "{$proxy_path}/data:/data", + ], ], ], ]; } else { - $config['services']['traefik']['command'][] = "--providers.docker=true"; + return null; } + $config = Yaml::dump($config, 12, 2); SaveConfiguration::run($server, $config); return $config; } function setup_dynamic_configuration() { - $dynamic_config_path = get_proxy_path() . "/dynamic"; $settings = InstanceSettings::get(); $server = Server::find(0); + $dynamic_config_path = $server->proxyPath() . "/dynamic"; if ($server) { $file = "$dynamic_config_path/coolify.yaml"; if (empty($settings->fqdn)) { @@ -308,7 +341,7 @@ function setup_dynamic_configuration() } function setup_default_redirect_404(string|null $redirect_url, Server $server) { - $traefik_dynamic_conf_path = get_proxy_path() . "/dynamic"; + $traefik_dynamic_conf_path = $server->proxyPath() . "/dynamic"; $traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml"; if (empty($redirect_url)) { instant_remote_process([ diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 621251d36..359792ddc 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1056,6 +1056,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName + )); } } if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { @@ -1495,7 +1505,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $preview_fqdn; }); } - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns, serviceLabels: $serviceLabels)); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + serviceLabels: $serviceLabels + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $uuid, + domains: $fqdns, + serviceLabels: $serviceLabels + )); } } } diff --git a/config/sentry.php b/config/sentry.php index 74eb9ba6a..2d2d440c3 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,7 +3,7 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://1bbc8f762199a52aee39196adb3e8d1a@o1082494.ingest.sentry.io/4505347448045568', + 'dsn' => 'https://f0b0e6be13926d4ac68d68d51d38db8f@o1082494.ingest.us.sentry.io/4505347448045568', // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index a163bf05f..94c8c9755 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -4,11 +4,13 @@ href="{{ route('server.proxy', $parameters) }}"> - @if (data_get($server, 'proxy.type') !== 'NONE') - - - + @if ($server->proxyType() !== 'NONE') + @if ($server->proxyType() === 'TRAEFIK_V2') + + + + @endif diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index e87ad573a..7c6599792 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -1,16 +1,24 @@
@if (data_get($server, 'proxy.type'))
- @if ($selectedProxy === 'TRAEFIK_V2') + @if ($selectedProxy !== 'NONE')

Configuration

- Save @if ($server->proxy->status === 'exited') Switch Proxy + @else + Switch Proxy @endif + Save +
-
Traefik v2
+
+ @if ($server->proxyType() === 'TRAEFIK_V2') +
Traefik v2
+ @elseif ($server->proxyType() === 'CADDY') +
Caddy
+ @endif @if ( $server->proxy->last_applied_settings && $server->proxy->last_saved_settings !== $server->proxy->last_applied_settings) @@ -18,15 +26,18 @@ configurations.
@endif - + @if ($server->proxyType() === 'TRAEFIK_V2') + + @endif
@if ($proxy_settings)
- Reset configuration to default @@ -40,7 +51,7 @@

Configuration

Switch Proxy
-
Custom (None) Proxy Selected
+
Custom (None) Proxy Selected
@else

Configuration

@@ -57,14 +68,13 @@ Traefik - v2 + + + Caddy (experimental) Nginx - - Caddy -
@endif