diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 2e08ec6ad9..018a10573c 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -98,11 +98,11 @@ public function handle(Server $server) ]); if ($server->isSwarm()) { $command = $command->merge([ - 'docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true', + dockerNetworkCreateCommand('coolify-overlay', isSwarm: true, quiet: true).' || true', ]); } else { $command = $command->merge([ - 'docker network create --attachable coolify >/dev/null 2>&1 || true', + dockerNetworkCreateCommand('coolify', quiet: true).' || true', ]); $command = $command->merge([ "echo 'Done!'", diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 17948d93b9..48d15795c2 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -33,7 +33,7 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s } if ($service->networks()->count() > 0) { $commands[] = "echo 'Creating Docker network.'"; - $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; + $commands[] = dockerNetworkCreateCommand($service->uuid); } $commands[] = 'echo Starting service.'; $commands[] = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --remove-orphans --force-recreate --build"; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c2be064c41..395ac54df5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -793,7 +793,7 @@ private function deploy_docker_compose_buildpack() // TODO } else { $this->execute_remote_command([ - "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", + dockerNetworkCreateCommand($networkId, quiet: true).' || true', 'hidden' => true, 'ignore_errors' => true, ], [ diff --git a/app/Models/Server.php b/app/Models/Server.php index 74e8ba5b08..3aaa9e41d8 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1406,9 +1406,9 @@ public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false) return; } if ($isSwarm) { - return instant_remote_process(['docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true'], $this, false); + return instant_remote_process([dockerNetworkCreateCommand('coolify-overlay', isSwarm: true, quiet: true).' || true'], $this, false); } else { - return instant_remote_process(['docker network create coolify --attachable >/dev/null 2>&1 || true'], $this, false); + return instant_remote_process([dockerNetworkCreateCommand('coolify', quiet: true).' || true'], $this, false); } } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index ed18dfe760..ad27701287 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -4,6 +4,7 @@ use App\Enums\ProxyTypes; use App\Models\Application; use App\Models\Server; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Symfony\Component\Yaml\Yaml; @@ -21,6 +22,19 @@ function isDockerPredefinedNetwork(string $network): bool return in_array($network, ['default', 'host'], true); } +function dockerNetworkCreateCommand(string $network, bool $isSwarm = false, bool $quiet = false): string +{ + $safe = escapeshellarg($network); + $output = $quiet ? ' >/dev/null 2>&1' : ''; + $inspect = "docker network inspect {$safe} >/dev/null 2>&1"; + + if ($isSwarm) { + return "{$inspect} || docker network create --driver overlay --attachable {$safe}{$output}"; + } + + return "{$inspect} || docker network create --attachable --ipv6 {$safe}{$output} || {$inspect} || docker network create --attachable {$safe}{$output}"; +} + function collectProxyDockerNetworksByServer(Server $server) { if (! $server->isFunctional()) { @@ -110,8 +124,9 @@ function connectProxyToNetworks(Server $server) if ($server->isSwarm()) { $commands = $networks->map(function ($network) { $safe = escapeshellarg($network); + return [ - "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --driver overlay --attachable {$safe} >/dev/null", + dockerNetworkCreateCommand($network, isSwarm: true, quiet: true), "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true", "echo 'Successfully connected coolify-proxy to {$safe} network.'", ]; @@ -119,8 +134,9 @@ function connectProxyToNetworks(Server $server) } else { $commands = $networks->map(function ($network) { $safe = escapeshellarg($network); + return [ - "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --attachable {$safe} >/dev/null", + dockerNetworkCreateCommand($network, quiet: true), "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true", "echo 'Successfully connected coolify-proxy to {$safe} network.'", ]; @@ -135,7 +151,7 @@ function connectProxyToNetworks(Server $server) * 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 + * @return Collection Commands to create networks if they don't exist */ function ensureProxyNetworksExist(Server $server) { @@ -144,17 +160,19 @@ function ensureProxyNetworksExist(Server $server) if ($server->isSwarm()) { $commands = $networks->map(function ($network) { $safe = escapeshellarg($network); + return [ "echo 'Ensuring network {$safe} exists...'", - "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable {$safe}", + dockerNetworkCreateCommand($network, isSwarm: true), ]; }); } else { $commands = $networks->map(function ($network) { $safe = escapeshellarg($network); + return [ "echo 'Ensuring network {$safe} exists...'", - "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable {$safe}", + dockerNetworkCreateCommand($network), ]; }); } @@ -211,7 +229,7 @@ function extractCustomProxyCommands(Server $server, string $existing_config): ar $custom_commands[] = $command; } } - } catch (\Exception $e) { + } catch (Exception $e) { // If we can't parse the config, return empty array // Silently fail to avoid breaking the proxy regeneration } @@ -432,7 +450,7 @@ function getExactTraefikVersionFromContainer(Server $server): ?string Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Could not detect exact version"); return null; - } catch (\Exception $e) { + } catch (Exception $e) { Log::error("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage()); return null; @@ -479,7 +497,7 @@ function getTraefikVersionFromDockerCompose(Server $server): ?string Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Image format doesn't match expected pattern: {$image}"); return null; - } catch (\Exception $e) { + } catch (Exception $e) { Log::error("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage()); return null; diff --git a/tests/Unit/ProxyHelperTest.php b/tests/Unit/ProxyHelperTest.php index 3d5da695cb..67fc04358d 100644 --- a/tests/Unit/ProxyHelperTest.php +++ b/tests/Unit/ProxyHelperTest.php @@ -189,3 +189,20 @@ // only filters 'default' and 'host', so we maintain consistency expect(isDockerPredefinedNetwork('none'))->toBeFalse(); }); + +it('creates standalone docker networks with ipv6 fallback', function () { + $command = dockerNetworkCreateCommand('coolify'); + $safe = escapeshellarg('coolify'); + + expect($command)->toContain("docker network inspect {$safe}") + ->and($command)->toContain("docker network create --attachable --ipv6 {$safe}") + ->and($command)->toContain("docker network create --attachable {$safe}"); +}); + +it('keeps swarm docker network creation on overlay without ipv6 fallback', function () { + $command = dockerNetworkCreateCommand('coolify-overlay', isSwarm: true); + $safe = escapeshellarg('coolify-overlay'); + + expect($command)->toContain("docker network create --driver overlay --attachable {$safe}") + ->and($command)->not->toContain('--ipv6'); +});