2023-05-24 12:26:50 +00:00
< ? php
2025-09-09 10:52:19 +00:00
use App\Actions\Proxy\SaveProxyConfiguration ;
2024-08-08 11:20:24 +00:00
use App\Enums\ProxyTypes ;
2023-11-28 11:48:55 +00:00
use App\Models\Application ;
2023-05-24 12:26:50 +00:00
use App\Models\Server ;
use Symfony\Component\Yaml\Yaml ;
2025-11-28 16:53:26 +00:00
/**
* Check if a network name is a Docker predefined system network .
* These networks cannot be created , modified , or managed by docker network commands .
*
* @ param string $network Network name to check
* @ return bool True if it ' s a predefined network that should be skipped
*/
function isDockerPredefinedNetwork ( string $network ) : bool
{
// Only filter 'default' and 'host' to match existing codebase patterns
// See: bootstrap/helpers/parsers.php:891, bootstrap/helpers/shared.php:689,748
return in_array ( $network , [ 'default' , 'host' ], true );
}
2024-07-11 08:16:56 +00:00
function collectProxyDockerNetworksByServer ( Server $server )
{
if ( ! $server -> isFunctional ()) {
return collect ();
}
$proxyType = $server -> proxyType ();
if ( is_null ( $proxyType ) || $proxyType === 'NONE' ) {
return collect ();
}
$networks = instant_remote_process ([ 'docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy' ], $server , false );
2024-10-31 17:20:11 +00:00
return collect ( $networks ) -> map ( function ( $network ) {
2024-07-11 08:16:56 +00:00
return collect ( json_decode ( $network )) -> keys ();
}) -> flatten () -> unique ();
}
function collectDockerNetworksByServer ( Server $server )
2023-09-25 07:17:42 +00:00
{
2024-08-05 11:45:24 +00:00
$allNetworks = collect ([]);
2023-12-15 14:48:01 +00:00
if ( $server -> isSwarm ()) {
$networks = collect ( $server -> swarmDockers ) -> map ( function ( $docker ) {
return $docker [ 'network' ];
});
} else {
// Standalone networks
$networks = collect ( $server -> standaloneDockers ) -> map ( function ( $docker ) {
return $docker [ 'network' ];
});
}
2024-08-05 11:45:24 +00:00
$allNetworks = $allNetworks -> merge ( $networks );
2023-11-27 08:58:31 +00:00
// Service networks
2023-11-28 11:48:55 +00:00
foreach ( $server -> services () -> get () as $service ) {
2024-08-01 11:47:58 +00:00
if ( $service -> isRunning ()) {
$networks -> push ( $service -> networks ());
}
2024-08-05 11:45:24 +00:00
$allNetworks -> push ( $service -> networks ());
2023-11-27 08:58:31 +00:00
}
2023-11-28 11:48:55 +00:00
// Docker compose based apps
$docker_compose_apps = $server -> dockerComposeBasedApplications ();
foreach ( $docker_compose_apps as $app ) {
2024-08-01 11:47:58 +00:00
if ( $app -> isRunning ()) {
$networks -> push ( $app -> uuid );
}
2024-08-05 11:45:24 +00:00
$allNetworks -> push ( $app -> uuid );
2023-11-28 11:48:55 +00:00
}
// Docker compose based preview deployments
$docker_compose_previews = $server -> dockerComposeBasedPreviewDeployments ();
foreach ( $docker_compose_previews as $preview ) {
2024-08-01 11:47:58 +00:00
if ( ! $preview -> isRunning ()) {
continue ;
}
2023-11-28 11:48:55 +00:00
$pullRequestId = $preview -> pull_request_id ;
$applicationId = $preview -> application_id ;
$application = Application :: find ( $applicationId );
2024-06-10 20:43:34 +00:00
if ( ! $application ) {
2023-11-28 11:48:55 +00:00
continue ;
}
$network = " { $application -> uuid } - { $pullRequestId } " ;
$networks -> push ( $network );
2024-08-05 11:45:24 +00:00
$allNetworks -> push ( $network );
2023-11-28 11:48:55 +00:00
}
2025-11-28 16:53:26 +00:00
$networks = collect ( $networks ) -> flatten () -> unique () -> filter ( function ( $network ) {
return ! isDockerPredefinedNetwork ( $network );
});
$allNetworks = $allNetworks -> flatten () -> unique () -> filter ( function ( $network ) {
return ! isDockerPredefinedNetwork ( $network );
});
2023-12-15 14:48:01 +00:00
if ( $server -> isSwarm ()) {
if ( $networks -> count () === 0 ) {
$networks = collect ([ 'coolify-overlay' ]);
2024-08-05 11:45:24 +00:00
$allNetworks = collect ([ 'coolify-overlay' ]);
2023-12-15 14:48:01 +00:00
}
2024-07-11 08:16:56 +00:00
} else {
if ( $networks -> count () === 0 ) {
$networks = collect ([ 'coolify' ]);
2024-08-05 11:45:24 +00:00
$allNetworks = collect ([ 'coolify' ]);
2024-07-11 08:16:56 +00:00
}
}
2024-08-05 11:45:24 +00:00
return [
'networks' => $networks ,
'allNetworks' => $allNetworks ,
];
2024-07-11 08:16:56 +00:00
}
function connectProxyToNetworks ( Server $server )
{
2024-08-05 11:45:24 +00:00
[ 'networks' => $networks ] = collectDockerNetworksByServer ( $server );
2024-07-11 08:16:56 +00:00
if ( $server -> isSwarm ()) {
2023-12-15 14:48:01 +00:00
$commands = $networks -> map ( function ( $network ) {
return [
" docker network ls --format ' { { .Name}}' | grep '^ $network $ ' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null " ,
" docker network connect $network coolify-proxy >/dev/null 2>&1 || true " ,
2024-09-27 13:36:51 +00:00
" echo 'Successfully connected coolify-proxy to $network network.' " ,
2023-12-15 14:48:01 +00:00
];
});
} else {
$commands = $networks -> map ( function ( $network ) {
return [
" 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 " ,
2024-09-27 13:36:51 +00:00
" echo 'Successfully connected coolify-proxy to $network network.' " ,
2023-12-15 14:48:01 +00:00
];
});
2023-09-22 06:42:27 +00:00
}
2023-12-15 14:48:01 +00:00
2023-09-22 06:42:27 +00:00
return $commands -> flatten ();
}
2025-11-27 08:04:42 +00:00
/**
* Ensures all required networks exist before docker compose up .
* This must be called BEFORE docker compose up since the compose file declares networks as external .
*
* @ param Server $server The server to ensure networks on
* @ return \Illuminate\Support\Collection Commands to create networks if they don ' t exist
*/
function ensureProxyNetworksExist ( Server $server )
{
[ 'allNetworks' => $networks ] = collectDockerNetworksByServer ( $server );
if ( $server -> isSwarm ()) {
$commands = $networks -> map ( function ( $network ) {
2026-03-04 10:36:52 +00:00
return [
" echo 'Ensuring network $network exists...' " ,
" docker network ls --format ' { { .Name}}' | grep -q '^ { $network } $ ' || docker network create --driver overlay --attachable $network " ,
];
2025-11-27 08:04:42 +00:00
});
} else {
$commands = $networks -> map ( function ( $network ) {
2026-03-04 10:36:52 +00:00
return [
" echo 'Ensuring network $network exists...' " ,
" docker network ls --format ' { { .Name}}' | grep -q '^ { $network } $ ' || docker network create --attachable $network " ,
];
2025-11-27 08:04:42 +00:00
});
}
return $commands -> flatten ();
}
2025-10-07 09:11:13 +00:00
function extractCustomProxyCommands ( Server $server , string $existing_config ) : array
{
$custom_commands = [];
$proxy_type = $server -> proxyType ();
if ( $proxy_type !== ProxyTypes :: TRAEFIK -> value || empty ( $existing_config )) {
return $custom_commands ;
}
try {
$yaml = Yaml :: parse ( $existing_config );
$existing_commands = data_get ( $yaml , 'services.traefik.command' , []);
if ( empty ( $existing_commands )) {
return $custom_commands ;
}
// Define default commands that Coolify generates
$default_command_prefixes = [
'--ping=' ,
'--api.' ,
'--entrypoints.http.address=' ,
'--entrypoints.https.address=' ,
'--entrypoints.http.http.encodequerysemicolons=' ,
'--entryPoints.http.http2.maxConcurrentStreams=' ,
'--entrypoints.https.http.encodequerysemicolons=' ,
'--entryPoints.https.http2.maxConcurrentStreams=' ,
'--entrypoints.https.http3' ,
'--providers.file.' ,
'--certificatesresolvers.' ,
'--providers.docker' ,
'--providers.swarm' ,
'--log.level=' ,
'--accesslog.' ,
];
// Extract commands that don't match default prefixes (these are custom)
foreach ( $existing_commands as $command ) {
$is_default = false ;
foreach ( $default_command_prefixes as $prefix ) {
if ( str_starts_with ( $command , $prefix )) {
$is_default = true ;
break ;
}
}
if ( ! $is_default ) {
$custom_commands [] = $command ;
}
}
} catch ( \Exception $e ) {
// If we can't parse the config, return empty array
// Silently fail to avoid breaking the proxy regeneration
}
return $custom_commands ;
}
function generateDefaultProxyConfiguration ( Server $server , array $custom_commands = [])
2023-06-22 12:18:17 +00:00
{
2024-03-11 14:08:05 +00:00
$proxy_path = $server -> proxyPath ();
$proxy_type = $server -> proxyType ();
2023-11-29 09:06:52 +00:00
if ( $server -> isSwarm ()) {
$networks = collect ( $server -> swarmDockers ) -> map ( function ( $docker ) {
return $docker [ 'network' ];
}) -> unique ();
2023-12-18 13:01:25 +00:00
if ( $networks -> count () === 0 ) {
$networks = collect ([ 'coolify-overlay' ]);
}
2023-11-29 09:06:52 +00:00
} else {
$networks = collect ( $server -> standaloneDockers ) -> map ( function ( $docker ) {
return $docker [ 'network' ];
}) -> unique ();
2023-12-18 13:01:25 +00:00
if ( $networks -> count () === 0 ) {
$networks = collect ([ 'coolify' ]);
}
2023-11-29 09:06:52 +00:00
}
2023-12-18 13:01:25 +00:00
2023-06-22 12:18:17 +00:00
$array_of_networks = collect ([]);
2025-08-18 15:48:24 +00:00
$filtered_networks = collect ([]);
$networks -> map ( function ( $network ) use ( $array_of_networks , $filtered_networks ) {
2025-11-28 16:53:26 +00:00
if ( isDockerPredefinedNetwork ( $network )) {
return ; // Predefined networks cannot be used in network configuration
2025-08-18 15:48:24 +00:00
}
2023-06-22 12:18:17 +00:00
$array_of_networks [ $network ] = [
2024-06-10 20:43:34 +00:00
'external' => true ,
2023-06-22 12:18:17 +00:00
];
2025-08-18 15:48:24 +00:00
$filtered_networks -> push ( $network );
2023-06-22 12:18:17 +00:00
});
2024-08-07 15:52:51 +00:00
if ( $proxy_type === ProxyTypes :: TRAEFIK -> value ) {
2024-03-11 14:08:05 +00:00
$labels = [
2024-06-10 20:43:34 +00:00
'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' ,
2024-09-28 09:14:14 +00:00
'coolify.proxy=true' ,
2024-03-11 14:08:05 +00:00
];
$config = [
2025-02-03 20:24:01 +00:00
'name' => 'coolify-proxy' ,
2024-06-10 20:43:34 +00:00
'networks' => $array_of_networks -> toArray (),
'services' => [
'traefik' => [
'container_name' => 'coolify-proxy' ,
2025-11-14 08:31:07 +00:00
'image' => 'traefik:v3.6' ,
2024-06-10 20:43:34 +00:00
'restart' => RESTART_MODE ,
'extra_hosts' => [
'host.docker.internal:host-gateway' ,
2024-03-11 14:08:05 +00:00
],
2025-08-18 15:48:24 +00:00
'networks' => $filtered_networks -> toArray (),
2024-06-10 20:43:34 +00:00
'ports' => [
'80:80' ,
'443:443' ,
2024-10-10 17:47:01 +00:00
'443:443/udp' ,
2024-06-10 20:43:34 +00:00
'8080:8080' ,
2024-03-11 14:08:05 +00:00
],
2024-06-10 20:43:34 +00:00
'healthcheck' => [
'test' => 'wget -qO- http://localhost:80/ping || exit 1' ,
'interval' => '4s' ,
'timeout' => '2s' ,
'retries' => 5 ,
2024-03-11 14:08:05 +00:00
],
2024-06-10 20:43:34 +00:00
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro' ,
2024-12-06 12:07:56 +00:00
2024-03-11 14:08:05 +00:00
],
2024-06-10 20:43:34 +00:00
'command' => [
'--ping=true' ,
'--ping.entrypoint=http' ,
'--api.dashboard=true' ,
'--entrypoints.http.address=:80' ,
'--entrypoints.https.address=:443' ,
'--entrypoints.http.http.encodequerysemicolons=true' ,
2025-02-02 13:03:18 +00:00
'--entryPoints.http.http2.maxConcurrentStreams=250' ,
2024-06-10 20:43:34 +00:00
'--entrypoints.https.http.encodequerysemicolons=true' ,
2025-02-02 13:03:18 +00:00
'--entryPoints.https.http2.maxConcurrentStreams=250' ,
2024-10-10 17:47:01 +00:00
'--entrypoints.https.http3' ,
2024-06-10 20:43:34 +00:00
'--providers.file.directory=/traefik/dynamic/' ,
'--providers.file.watch=true' ,
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true' ,
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http' ,
2024-12-06 12:07:56 +00:00
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json' ,
2024-03-11 14:08:05 +00:00
],
2024-06-10 20:43:34 +00:00
'labels' => $labels ,
2023-06-22 12:18:17 +00:00
],
],
2024-03-11 14:08:05 +00:00
];
if ( isDev ()) {
2024-12-06 12:07:56 +00:00
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--api.insecure=true' ;
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--log.level=debug' ;
2024-06-10 20:43:34 +00:00
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--accesslog.filepath=/traefik/access.log' ;
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--accesslog.bufferingsize=100' ;
2025-05-05 06:55:44 +00:00
$config [ 'services' ][ 'traefik' ][ 'volumes' ][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik' ;
2024-12-06 12:07:56 +00:00
} else {
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--api.insecure=false' ;
$config [ 'services' ][ 'traefik' ][ 'volumes' ][] = " { $proxy_path } :/traefik " ;
2024-03-11 14:08:05 +00:00
}
if ( $server -> isSwarm ()) {
data_forget ( $config , 'services.traefik.container_name' );
data_forget ( $config , 'services.traefik.restart' );
data_forget ( $config , 'services.traefik.labels' );
2023-11-29 09:06:52 +00:00
2025-01-23 14:57:34 +00:00
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--providers.swarm.endpoint=unix:///var/run/docker.sock' ;
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--providers.swarm.exposedbydefault=false' ;
2024-03-11 14:08:05 +00:00
$config [ 'services' ][ 'traefik' ][ 'deploy' ] = [
2024-06-10 20:43:34 +00:00
'labels' => $labels ,
'placement' => [
'constraints' => [
'node.role==manager' ,
2024-03-11 14:08:05 +00:00
],
],
];
} else {
2024-06-10 20:43:34 +00:00
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--providers.docker=true' ;
2025-01-23 14:57:34 +00:00
$config [ 'services' ][ 'traefik' ][ 'command' ][] = '--providers.docker.exposedbydefault=false' ;
2024-03-11 14:08:05 +00:00
}
2025-10-07 09:11:13 +00:00
// Append custom commands (e.g., trustedIPs for Cloudflare)
if ( ! empty ( $custom_commands )) {
foreach ( $custom_commands as $custom_command ) {
$config [ 'services' ][ 'traefik' ][ 'command' ][] = $custom_command ;
}
}
2024-06-10 20:43:34 +00:00
} elseif ( $proxy_type === 'CADDY' ) {
2024-03-11 14:08:05 +00:00
$config = [
2024-06-10 20:43:34 +00:00
'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' ,
2024-03-11 14:08:05 +00:00
],
2024-06-10 20:43:34 +00:00
'environment' => [
'CADDY_DOCKER_POLLING_INTERVAL=5s' ,
'CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile' ,
2024-03-11 14:36:45 +00:00
],
2025-08-18 15:48:24 +00:00
'networks' => $filtered_networks -> toArray (),
2024-06-10 20:43:34 +00:00
'ports' => [
'80:80' ,
'443:443' ,
2024-10-09 16:34:17 +00:00
'443:443/udp' ,
2024-03-11 14:08:05 +00:00
],
2024-09-27 13:36:51 +00:00
'labels' => [
'coolify.managed=true' ,
2024-10-14 19:35:20 +00:00
'coolify.proxy=true' ,
2024-09-27 13:36:51 +00:00
],
2024-06-10 20:43:34 +00:00
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro' ,
2024-03-11 16:17:34 +00:00
" { $proxy_path } /dynamic:/dynamic " ,
2024-03-11 14:08:05 +00:00
" { $proxy_path } /config:/config " ,
" { $proxy_path } /data:/data " ,
],
2023-11-28 17:42:09 +00:00
],
],
];
} else {
2024-03-11 14:08:05 +00:00
return null ;
2023-11-28 17:42:09 +00:00
}
2024-03-11 14:08:05 +00:00
2023-11-29 09:06:52 +00:00
$config = Yaml :: dump ( $config , 12 , 2 );
2025-09-09 10:52:19 +00:00
SaveProxyConfiguration :: run ( $server , $config );
2024-06-10 20:43:34 +00:00
2023-09-25 07:17:42 +00:00
return $config ;
2023-06-22 12:18:17 +00:00
}
2025-11-13 12:38:57 +00:00
function getExactTraefikVersionFromContainer ( Server $server ) : ? string
{
try {
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Checking for exact version " );
// Method A: Execute traefik version command (most reliable)
$versionCommand = " docker exec coolify-proxy traefik version 2>/dev/null | grep -oP 'Version: \ s+ \ K \ d+ \ . \ d+ \ . \ d+' " ;
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Running: { $versionCommand } " );
$output = instant_remote_process ([ $versionCommand ], $server , false );
if ( ! empty ( trim ( $output ))) {
$version = trim ( $output );
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Detected exact version from command: { $version } " );
return $version ;
}
// Method B: Try OCI label as fallback
$labelCommand = " docker inspect coolify-proxy --format ' { { index .Config.Labels \" org.opencontainers.image.version \" }}' 2>/dev/null " ;
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Trying OCI label " );
$label = instant_remote_process ([ $labelCommand ], $server , false );
if ( ! empty ( trim ( $label ))) {
// Extract version number from label (might have 'v' prefix)
if ( preg_match ( '/(\d+\.\d+\.\d+)/' , trim ( $label ), $matches )) {
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Detected from OCI label: { $matches [ 1 ] } " );
return $matches [ 1 ];
}
}
Log :: debug ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Could not detect exact version " );
return null ;
} catch ( \Exception $e ) {
Log :: error ( " getExactTraefikVersionFromContainer: Server ' { $server -> name } ' (ID: { $server -> id } ) - Error: " . $e -> getMessage ());
return null ;
}
}
function getTraefikVersionFromDockerCompose ( Server $server ) : ? string
{
try {
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Starting version detection " );
// Try to get exact version from running container (e.g., "3.6.0")
$exactVersion = getExactTraefikVersionFromContainer ( $server );
if ( $exactVersion ) {
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Using exact version: { $exactVersion } " );
return $exactVersion ;
}
// Fallback: Check image tag (current method)
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Falling back to image tag detection " );
$containerName = 'coolify-proxy' ;
$inspectCommand = " docker inspect { $containerName } --format ' { { .Config.Image}}' 2>/dev/null " ;
$image = instant_remote_process ([ $inspectCommand ], $server , false );
if ( empty ( trim ( $image ))) {
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Container ' { $containerName } ' not found or not running " );
return null ;
}
$image = trim ( $image );
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Running container image: { $image } " );
// Extract version from image string (e.g., "traefik:v3.6" or "traefik:3.6.0" or "traefik:latest")
if ( preg_match ( '/traefik:(v?\d+\.\d+(?:\.\d+)?|latest)/i' , $image , $matches )) {
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Extracted version from image tag: { $matches [ 1 ] } " );
return $matches [ 1 ];
}
Log :: debug ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Image format doesn't match expected pattern: { $image } " );
return null ;
} catch ( \Exception $e ) {
Log :: error ( " getTraefikVersionFromDockerCompose: Server ' { $server -> name } ' (ID: { $server -> id } ) - Error: " . $e -> getMessage ());
return null ;
}
}