Skip to content

Instantly share code, notes, and snippets.

@erikvullings
Last active May 5, 2025 09:47
Show Gist options
  • Save erikvullings/34b728e4987a2f0f57aded96836bc074 to your computer and use it in GitHub Desktop.
Save erikvullings/34b728e4987a2f0f57aded96836bc074 to your computer and use it in GitHub Desktop.
Docker swarm setup on Windows using WSL2

WSL2 Docker Swarm Setup Scripts

This repository contains PowerShell scripts to help configure and run a multi-node Docker Swarm cluster using WSL2 on Windows 11, without relying on Docker Desktop. If you need GPU access in your swarm, please read Tom Lankhort's gist.

These scripts automate:

  • Port forwarding using netsh (TCP and UDP)
  • Generation of config.toml for wsl2proxy
  • Service installation and startup of wsl2proxy
  • Docker Swarm initialization (manager)
  • Docker Swarm joining (worker).

🛠️ Requirements

  • Windows 11 with WSL2
  • Ubuntu installed inside WSL2
  • Docker CE installed inside WSL2 (not Docker Desktop)

📂 Files

Script Description
common-network-setup.ps1 Shared helper for port forwarding and wsl2proxy
setup-swarm-manager.ps1 Sets up the manager node and initializes the Swarm
setup-swarm-worker.ps1 Sets up the worker node and joins it to the manager

🚀 Usage

1. Download these scripts

Optionally, leave the swarm.

docker swarm leave
# docker swarm leave --force # Optionally, force to leave

2. Run on the manager

  • Detects IPs
  • Sets up TCP/UDP forwarding
  • Installs and starts wsl2proxy
  • Initializes Swarm

In order to run these downloaded scripts as admin, you may have to give Windows permission to do so.

# Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
.\setup-swarm-manager.ps1

3. Run on each worker

Note that you need to include the join command from your manager.

.\setup-swarm-worker.ps1 -JoinCommand "docker swarm join --token SWMTKN-1-abcdefg12345... 192.168.1.100:2377"

Replace 192.168.1.100 with the Windows LAN IP (or DNS name) of your Swarm manager

🔁 Making It Persistent

To run these scripts at startup:

  • Open Task Scheduler
  • Create a new task to run setup-swarm-manager.ps1 or setup-swarm-worker.ps1
  • Set trigger to At logon or At startup
  • Ensure it runs with Administrator privileges

🧠 Notes

  • You must run the scripts as administrator
  • wsl2proxy is required for forwarding UDP ports (which Docker Swarm uses for overlay networking)
  • TCP ports are forwarded via netsh
  • This setup does not require Docker Desktop
<#
.SYNOPSIS
Configures port forwarding and firewall rules for Docker Swarm communication with WSL2.
.DESCRIPTION
This script automates the process of setting up port forwarding from the Windows host
to a WSL2 Ubuntu instance and configures Windows Firewall rules to allow inbound
traffic for the ports required by Docker Swarm.
.NOTES
Author: Gemini
Date: 2025-05-01
Requires: Administrator privileges
.PARAMETER Action
Specifies whether to 'Add' or 'Remove' the port forwarding and firewall rules.
Valid values are 'Add' and 'Remove'.
.EXAMPLE
.\Configure-DockerSwarmPorts.ps1 -Action Add
.EXAMPLE
.\Configure-DockerSwarmPorts.ps1 -Action Remove
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateSet('Add', 'Remove')]
[string]$Action
)
# Define the Docker Swarm ports and protocols
$SwarmPorts = @(
@{ Port = 2377; Protocol = 'TCP'; Description = 'Docker Swarm Cluster Management' }
@{ Port = 7946; Protocol = 'TCP'; Description = 'Docker Swarm Node Communication (TCP)' }
@{ Port = 7946; Protocol = 'UDP'; Description = 'Docker Swarm Node Communication (UDP)' }
@{ Port = 4789; Protocol = 'UDP'; Description = 'Docker Swarm Overlay Network Traffic' }
)
# Get the WSL2 IP address
function Get-WslIPAddress {
try {
$wslIP = wsl.exe hostname -I | Where-Object { $_ -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' }
if (-not $wslIP) {
Write-Error "Could not determine WSL2 IP address. Make sure your WSL2 instance is running."
exit 1
}
# Take the first IP address if multiple are returned
$wslIP = $wslIP.Split(' ')[0]
return $wslIP.Trim()
} catch {
Write-Error "An error occurred while getting the WSL2 IP address: $($_.Exception.Message)"
exit 1
}
}
$WSL2IP = Get-WslIPAddress
Write-Host "WSL2 IP Address detected: $($WSL2IP)"
if ($Action -eq 'Add') {
Write-Host "Adding port forwarding and firewall rules..."
foreach ($PortRule in $SwarmPorts) {
$port = $PortRule.Port
$protocol = $PortRule.Protocol
$description = $PortRule.Description
$ruleName = "Docker Swarm - $($description)"
# Add port forwarding rule
Write-Host "Adding port forwarding rule for $($protocol) port $($port)..."
try {
# Check if rule already exists
$existingProxyRule = netsh interface portproxy show v4tov4 | Select-String "Listen on IPv4: .* $($port)" | Select-String "Connect to IPv4: .* $($WSL2IP)"
if (-not $existingProxyRule) {
netsh interface portproxy add v4tov4 listenport=$port listenaddress=0.0.0.0 connectport=$port connectaddress=$WSL2IP
Write-Host "Successfully added port forwarding rule for $($protocol) port $($port)."
} else {
Write-Host "Port forwarding rule for $($protocol) port $($port) to $($WSL2IP) already exists."
}
} catch {
Write-Error "Failed to add port forwarding rule for $($protocol) port $($port): $($_.Exception.Message)"
}
# Add Windows Firewall rule
Write-Host "Adding Windows Firewall rule for $($protocol) port $($port)..."
try {
# Check if firewall rule already exists
$existingFirewallRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if (-not $existingFirewallRule) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol $protocol -LocalPort $port -RemoteAddress Any
Write-Host "Successfully added Windows Firewall rule for $($protocol) port $($port)."
} else {
Write-Host "Windows Firewall rule '$($ruleName)' already exists."
}
} catch {
Write-Error "Failed to add Windows Firewall rule for $($protocol) port $($port): $($_.Exception.Message)"
}
}
Write-Host "Port forwarding and firewall rules added."
} elseif ($Action -eq 'Remove') {
Write-Host "Removing port forwarding and firewall rules..."
foreach ($PortRule in $SwarmPorts) {
$port = $PortRule.Port
$protocol = $PortRule.Protocol
$description = $PortRule.Description
$ruleName = "Docker Swarm - $($description)"
# Remove port forwarding rule
Write-Host "Removing port forwarding rule for $($protocol) port $($port)..."
try {
# Check if rule exists before attempting removal
$existingProxyRule = netsh interface portproxy show v4tov4 | Select-String "Listen on IPv4: .* $($port)" # We don't check connectaddress here as it might have changed
if ($existingProxyRule) {
netsh interface portproxy delete v4tov4 listenport=$port listenaddress=0.0.0.0
Write-Host "Successfully removed port forwarding rule for $($protocol) port $($port)."
} else {
Write-Host "Port forwarding rule for $($protocol) port $($port) not found."
}
} catch {
Write-Error "Failed to remove port forwarding rule for $($protocol) port $($port): $($_.Exception.Message)"
}
# Remove Windows Firewall rule
Write-Host "Removing Windows Firewall rule for $($protocol) port $($port)..."
try {
$existingFirewallRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existingFirewallRule) {
Remove-NetFirewallRule -DisplayName $ruleName -Confirm:$false
Write-Host "Successfully removed Windows Firewall rule '$($ruleName)'."
} else {
Write-Host "Windows Firewall rule '$($ruleName)' not found."
}
} catch {
Write-Error "Failed to remove Windows Firewall rule for $($protocol) port $($port): $($_.Exception.Message)"
}
}
Write-Host "Port forwarding and firewall rules removed."
}
<#
.SYNOPSIS
Initializes a Docker Swarm manager on WSL2 using the Windows host's IP for advertising and WSL2 IP for listening.
.DESCRIPTION
This script identifies the Windows 11 host's IP address on the local network
using PowerShell cmdlets (for advertising), gets the WSL2 instance's IP address
(for listening), checks if a Docker Swarm is already active in the WSL2 instance,
and if not, initializes a new swarm advertising the host's IP and listening
on the WSL2 IP. It then displays the worker join token.
.NOTES
Author: Gemini
Date: 2025-05-01
Requires: Administrator privileges, running WSL2 instance with Docker installed.
.EXAMPLE
.\Setup-SwarmManager.ps1
#>
[CmdletBinding()]
param()
# Function to get the Windows host IP address accessible by other machines
function Get-WindowsHostIP {
Write-Host "Detecting Windows host IP address..."
try {
# Get all network adapters, exclude virtual and loopback adapters
$adapters = Get-NetAdapter | Where-Object {
$_.Status -eq 'Up' -and
$_.Virtual -eq $false -and
$_.Name -notmatch 'vEthernet \(WSL\)' -and
$_.Name -notmatch 'Docker Desktop' -and
$_.Name -notmatch 'Loopback'
}
if (-not $adapters) {
Write-Warning "No active, non-virtual network adapters found. Checking all non-loopback IPs."
# If no standard adapters found, check all non-loopback IPs
$ipAddresses = Get-NetIPAddress | Where-Object {
$_.AddressFamily -eq 'IPv4' -and
$_.IPAddress -ne '127.0.0.1' -and
$_.InterfaceAlias -notmatch 'vEthernet \(WSL\)' -and
$_.InterfaceAlias -notmatch 'Docker Desktop' -and
$_.InterfaceAlias -notmatch 'Loopback'
} | Select-Object -ExpandProperty IPAddress
} else {
# Get IPv4 addresses for the found adapters
$ipAddresses = Get-NetIPAddress | Where-Object {
$_.InterfaceIndex -in $adapters.IfIndex -and
$_.AddressFamily -eq 'IPv4' -and
$_.IPAddress -ne '127.0.0.1'
} | Select-Object -ExpandProperty IPAddress
}
if (-not $ipAddresses) {
Write-Error "Could not automatically determine a suitable Windows host IP address."
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
# Prioritize a non-private IP if available, otherwise take the first private one
$publicIp = $ipAddresses | Where-Object { $_ -notmatch '^10\.' -and $_ -notmatch '^172\.(1[6-9]|2[0-9]|3[0-1])\.' -and $_ -notmatch '^192\.168\.' } | Select-Object -First 1
if ($publicIp) {
$finalIp = $publicIp
if ($ipAddresses.Count -gt 1) {
Write-Host "Multiple potential IP addresses found: $($ipAddresses -join ', '). Selected non-private IP: $($finalIp)."
}
} else {
$finalIp = $ipAddresses | Select-Object -First 1
if ($ipAddresses.Count -gt 1) {
Write-Host "Multiple potential IP addresses found: $($ipAddresses -join ', '). No non-private IP found, selected first private IP: $($finalIp)."
}
}
if (-not $finalIp) {
Write-Error "Could not automatically determine a suitable Windows host IP address after filtering."
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
Write-Host "Windows host IP address found: $($finalIp)"
return $finalIp
} catch {
Write-Error "An error occurred while getting the Windows host IP address: $($_.Exception.Message)"
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
}
# Function to get the WSL2 IP address
function Get-WslIPAddress {
Write-Host "Detecting WSL2 IP address..."
try {
# Use wsl.exe to get the IP address
$wslIP = wsl.exe hostname -I | Where-Object { $_ -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' }
if (-not $wslIP) {
Write-Error "Could not determine WSL2 IP address. Make sure your WSL2 instance is running."
exit 1
}
# Take the first IP address if multiple are returned by hostname -I
$wslIP = $wslIP.Split(' ')[0]
Write-Host "WSL2 IP Address found: $($wslIP.Trim())"
return $wslIP.Trim()
} catch {
Write-Error "An error occurred while getting the WSL2 IP address: $($_.Exception.Message)"
exit 1
}
}
$WINDOWS_IP = Get-WindowsHostIP
$WSL2_IP = Get-WslIPAddress
# Check if Swarm is already initialized in WSL2
Write-Host "Checking Docker Swarm state in WSL2..."
# Use -c directly with the command string for better handling by wsl.exe
$swarmStateResult = wsl.exe bash -c "docker info --format '{{.Swarm.LocalNodeState}}'"
$swarmState = $swarmStateResult.Trim()
if ($swarmState -eq "inactive") {
Write-Output "Initializing Docker Swarm on manager..."
Write-Output "Advertising address: $($WINDOWS_IP)"
Write-Output "Listening address (WSL2): $($WSL2_IP):2377"
# Initialize Swarm using the detected Windows IP for advertise and WSL2 IP for listen
# Ensure the command is correctly quoted for bash
$initCommand = "docker swarm init --advertise-addr $($WINDOWS_IP) --listen-addr $($WSL2_IP):2377"
Write-Host "Executing in WSL2: $($initCommand)"
wsl.exe bash -c "$initCommand"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to initialize Docker Swarm. Check the output above for errors from the 'docker swarm init' command."
exit 1
}
Write-Host "Docker Swarm initialized successfully."
# Get the worker join token
Write-Host "Getting worker join token..."
# Use -c directly with the command string
$joinCommandResult = wsl.exe bash -c "docker swarm join-token worker"
$joinCommand = $joinCommandResult.Trim()
Write-Host "`n=================================================="
Write-Host "Swarm Manager Setup Complete!"
Write-Host "Run the following command on your worker nodes (in PowerShell as Administrator) to join the swarm:"
Write-Host "`n$($joinCommand)" # Output the full join command
Write-Host "=================================================="
} elseif ($swarmState -eq "active") {
Write-Output "Swarm is already active on this node."
# If active, still provide the join token in case it's needed
Write-Host "Getting worker join token..."
# Use -c directly with the command string
$joinCommandResult = wsl.exe bash -c "docker swarm join-token worker"
$joinCommand = $joinCommandResult.Trim()
Write-Host "`n=================================================="
Write-Host "Swarm is already active."
Write-Host "If you need to join other workers, run the following command on them (in PowerShell as Administrator):"
Write-Host "`n$($joinCommand)" # Output the full join command
Write-Host "=================================================="
} else {
Write-Warning "Unexpected Docker Swarm state: $($swarmState). Manual intervention may be required."
}
<#
.SYNOPSIS
Joins a WSL2 Docker instance to an existing Docker Swarm as a worker, advertising the Windows host's IP.
.DESCRIPTION
This script identifies the Windows 11 host's IP address on the local network,
takes the Docker Swarm join command (obtained from the manager), and executes
a modified join command within the WSL2 Ubuntu instance that includes advertising
the host's IP. It checks if the node is already part of a swarm.
.NOTES
Author: Gemini
Date: 2025-05-01
Requires: Administrator privileges, running WSL2 instance with Docker installed,
the join command from the Swarm manager.
.PARAMETER JoinCommand
The full 'docker swarm join ...' command obtained from the Swarm manager.
.EXAMPLE
.\Join-SwarmWorker.ps1 -JoinCommand "docker swarm join --token SWMTKN-1-abcdefg12345... 192.168.1.100:2377"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$JoinCommand
)
# Function to get the Windows host IP address accessible by other machines
function Get-WindowsHostIP {
Write-Host "Detecting Windows host IP address..."
try {
# Get all network adapters, exclude virtual and loopback adapters
$adapters = Get-NetAdapter | Where-Object {
$_.Status -eq 'Up' -and
$_.Virtual -eq $false -and
$_.Name -notmatch 'vEthernet \(WSL\)' -and
$_.Name -notmatch 'Docker Desktop' -and
$_.Name -notmatch 'Loopback'
}
if (-not $adapters) {
Write-Warning "No active, non-virtual network adapters found. Checking all non-loopback IPs."
# If no standard adapters found, check all non-loopback IPs
$ipAddresses = Get-NetIPAddress | Where-Object {
$_.AddressFamily -eq 'IPv4' -and
$_.IPAddress -ne '127.0.0.1' -and
$_.InterfaceAlias -notmatch 'vEthernet \(WSL\)' -and
$_.InterfaceAlias -notmatch 'Docker Desktop' -and
$_.InterfaceAlias -notmatch 'Loopback'
} | Select-Object -ExpandProperty IPAddress
} else {
# Get IPv4 addresses for the found adapters
$ipAddresses = Get-NetIPAddress | Where-Object {
$_.InterfaceIndex -in $adapters.IfIndex -and
$_.AddressFamily -eq 'IPv4' -and
$_.IPAddress -ne '127.0.0.1'
} | Select-Object -ExpandProperty IPAddress
}
if (-not $ipAddresses) {
Write-Error "Could not automatically determine a suitable Windows host IP address."
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
# Prioritize a non-private IP if available, otherwise take the first private one
$publicIp = $ipAddresses | Where-Object { $_ -notmatch '^10\.' -and $_ -notmatch '^172\.(1[6-9]|2[0-9]|3[0-1])\.' -and $_ -notmatch '^192\.168\.' } | Select-Object -First 1
if ($publicIp) {
$finalIp = $publicIp
if ($ipAddresses.Count -gt 1) {
Write-Host "Multiple potential IP addresses found: $($ipAddresses -join ', '). Selected non-private IP: $($finalIp)."
}
} else {
$finalIp = $ipAddresses | Select-Object -First 1
if ($ipAddresses.Count -gt 1) {
Write-Host "Multiple potential IP addresses found: $($ipAddresses -join ', '). No non-private IP found, selected first private IP: $($finalIp)."
}
}
if (-not $finalIp) {
Write-Error "Could not automatically determine a suitable Windows host IP address after filtering."
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
Write-Host "Windows host IP address found: $($finalIp)"
return $finalIp
} catch {
Write-Error "An error occurred while getting the Windows host IP address: $($_.Exception.Message)"
Write-Host "Please manually identify the correct IP address using 'ipconfig /all' in Command Prompt or PowerShell and update the script."
exit 1
}
}
$WORKER_WINDOWS_IP = Get-WindowsHostIP
Write-Host "Checking Docker Swarm state in WSL2..."
$swarmStateResult = wsl.exe bash -c "docker info --format '{{.Swarm.LocalNodeState}}'"
$swarmState = $swarmStateResult.Trim()
if ($swarmState -eq "inactive") {
Write-Output "Attempting to join Docker Swarm as a worker..."
# Parse the provided join command to extract the token and manager address
$joinCommandParts = $JoinCommand -split ' '
$token = ""
$managerAddress = ""
for ($i = 0; $i -lt $joinCommandParts.Length; $i++) {
if ($joinCommandParts[$i] -eq '--token') {
$token = $joinCommandParts[$i + 1]
} elseif ($joinCommandParts[$i] -match ':\d+') {
$managerAddress = $joinCommandParts[$i]
}
}
if ([string]::IsNullOrWhiteSpace($token) -or [string]::IsNullOrWhiteSpace($managerAddress)) {
Write-Error "Could not parse the provided join command. Please ensure it's the full output from 'docker swarm join-token worker'."
exit 1
}
# Construct the join command with the advertise address
$modifiedJoinCommand = "docker swarm join --token $($token) --advertise-addr $($WORKER_WINDOWS_IP) $($managerAddress)"
Write-Output "Executing join command in WSL2:"
Write-Output "$($modifiedJoinCommand)"
wsl.exe bash -c "$($modifiedJoinCommand)"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to join Docker Swarm. Check the output above for errors."
Write-Error "Ensure the manager node is accessible from this machine at $($managerAddress) and port forwarding is configured."
exit 1
}
Write-Host "Successfully joined the Docker Swarm as a worker."
} elseif ($swarmState -eq "active") {
Write-Output "This node is already part of a Docker Swarm."
Write-Host "You can check the swarm status using: docker node ls"
} else {
Write-Warning "Unexpected Docker Swarm state: $($swarmState). Manual intervention may be required."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment