Skip to content

Instantly share code, notes, and snippets.

@iongion
Last active December 9, 2025 11:13
Show Gist options
  • Select an option

  • Save iongion/f5dcd313ebdc0e73c14b61e039a96d78 to your computer and use it in GitHub Desktop.

Select an option

Save iongion/f5dcd313ebdc0e73c14b61e039a96d78 to your computer and use it in GitHub Desktop.
WSL home drive on real physical disk with ext4

Setup WSL home dir on custom real disk drive

Requirements

  • WSL with systemd enabled
  • ext4 physical drive name and partition number
  • your WSL username
  • And empty /home/<User> - if migrating, move it to /home/<User>.backup and then rsync the contents to the real physical drive after mounting

Actions

To install for a distro, default user and PHYSICALDRIVE1:

Run as Administrator

.\Setup-WslHomeSSD.ps1 -Mode install -Distribution "Ubuntu-24.04" -PhysicalDrive "\\.\PHYSICALDRIVE1"

It will:

  • Detect default user in that distro
  • Ask to confirm or override
  • Set everything up

Example, install with explicit user and partition 2:

.\Setup-WslHomeSSD.ps1 -Mode install `
  -Distribution "Ubuntu-24.04" `
  -User "istoica" `
  -PhysicalDrive "\\.\PHYSICALDRIVE2" `
  -Partition 2

Run as administrator

Uninstall for a distro:

.\Setup-WslHomeSSD.ps1 -Mode uninstall -Distribution "Ubuntu-24.04"

Summary

What happens in Windows

  1. It will write scripts inside C:\ProgramData\WslHomeMount\<Distribution>\ for example C:\ProgramData\WslHomeMount\Ubuntu-24.04\
  2. There one will find mount-ext4.ps1 that actually runs wsl --mount \\.\PHYSICALDRIVEX --partition <N> --type ext4 --options rw (logs to windows event log under source WSL-Disk-Tasks-<Distribution>)
  3. Creates a Windows Task Schedule task named WSL-HomeMount-<Distribution> - this one runs the script generated at 2 (powershell.exe -File "<mount-ext4.ps1>")
  4. Windows owns the PHYSICALDRIVE → WSL attach, but WSL/systemd decides when to call it.

What happens in WSL distro

  1. Creates this script /usr/local/sbin/mount-home-ssd.sh (Owned by root, chmod +x)
  2. Waits (up to ~20 seconds) for the SSD to appear at /mnt/wsl/PHYSICALDRIVEXp<Partition>
  3. If SSD is mounted, it does mount --bind /mnt/wsl/PHYSICALDRIVEXpN /home/<User> otherwise it fallsback to the default user home dir.
  4. Creates systemd units that call the windows scheduled task to mount the physical drive - This is the bridge from systemd to Windows!
  5. Creates /etc/systemd/system/home-<user>-ssd.service for calling the helper /usr/local/sbin/mount-home-ssd.sh once! This one waits for the physical drive and then final mounts it in /home/<User> overriding the default user home.
  6. Fallback home directory + sentinel files in /home/<User> - with a sentinel file /home/<User>/.FALLBACK_HOME_README - this file MUST never be present in the drive home dir.
param(
[Parameter(Mandatory = $true)]
[ValidateSet('install', 'uninstall')]
[string]$Mode,
[string]$Distribution,
[string]$User,
[string]$PhysicalDrive,
[int]$Partition = 1
)
# -------------------------------
# Helpers
# -------------------------------
function Assert-Admin {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "This script must be run as Administrator."
exit 1
}
}
function Select-Distribution {
$distros = wsl -l -q 2>$null
if (-not $distros) {
Write-Error "No WSL distributions found."
exit 1
}
Write-Host "Available WSL distributions:" -ForegroundColor Cyan
$i = 1
$map = @{}
foreach ($d in $distros) {
Write-Host " [$i] $d"
$map[$i] = $d
$i++
}
do {
$choice = Read-Host "Select distribution number"
[int]$idx = 0
[int]::TryParse($choice, [ref]$idx) | Out-Null
} while (-not $map.ContainsKey($idx))
return $map[$idx]
}
function Invoke-WslRoot {
param(
[string]$Distribution,
[string]$Command
)
& wsl -d $Distribution -u root -- /bin/sh -lc $Command
if ($LASTEXITCODE -ne 0) {
Write-Warning "Command failed in WSL ($Distribution): $Command"
}
}
function Invoke-WslUser {
param(
[string]$Distribution,
[string]$User,
[string]$Command
)
& wsl -d $Distribution -u $User -- /bin/sh -lc $Command
if ($LASTEXITCODE -ne 0) {
Write-Warning "Command failed in WSL ($Distribution, user=$User): $Command"
}
}
function Test-SystemdActive {
param(
[string]$Distribution
)
# This will start the distro if not already running
$checkCmd = @'
if [ -d /run/systemd/system ] && [ "$(ps -p 1 -o comm=)" = "systemd" ]; then
echo systemd
else
echo nosystemd
fi
'@
$out = & wsl -d $Distribution -- /bin/sh -lc $checkCmd 2>$null
if ($LASTEXITCODE -ne 0) {
return $false
}
return ($out.Trim() -eq 'systemd')
}
# -------------------------------
# Validate + fill parameters
# -------------------------------
Assert-Admin
if (-not $Distribution) {
$Distribution = Select-Distribution
}
if ($Mode -eq 'install' -and -not $PhysicalDrive) {
$PhysicalDrive = Read-Host "Enter physical drive path (e.g. \\.\PHYSICALDRIVE1)"
}
# Normalize PhysicalDrive for Linux mount path
$driveId = $null
if ($PhysicalDrive) {
# Expect form \\.\PHYSICALDRIVE1
if ($PhysicalDrive -match 'PHYSICALDRIVE(\d+)') {
$driveId = "PHYSICALDRIVE$($Matches[1])"
} else {
Write-Error "PhysicalDrive path '$PhysicalDrive' is not in expected form (\\.\PHYSICALDRIVE<N>)."
exit 1
}
}
if (-not $User -and $Mode -eq 'install') {
# Try to autodetect default user
$defaultUser = (& wsl -d $Distribution -- /bin/sh -lc 'id -un' 2>$null)
if ($LASTEXITCODE -eq 0 -and $defaultUser) {
Write-Host "Detected default user in distro '$Distribution': $defaultUser" -ForegroundColor Cyan
$useDetected = Read-Host "Use this user? (Y/n)"
if ($useDetected -match '^(n|no)$') {
$User = Read-Host "Enter Linux username to configure"
} else {
$User = $defaultUser.Trim()
}
} else {
$User = Read-Host "Enter Linux username to configure"
}
}
if ($Mode -eq 'install') {
if (-not $User) {
Write-Error "Linux user name is required for install."
exit 1
}
}
# Derived names/paths
$TaskName = "WSL-HomeMount-$Distribution"
$BaseDir = Join-Path $env:ProgramData "WslHomeMount\$Distribution"
$MountScriptPath = Join-Path $BaseDir "mount-ext4.ps1"
$LinuxRealHomeRoot = "/mnt/wsl/${driveId}p$Partition"
$LinuxMountScript = "/usr/local/sbin/mount-home-ssd.sh"
$HomeServiceName = "home-$User-ssd.service"
$HomeServicePath = "/etc/systemd/system/$HomeServiceName"
$WslMountServicePath = "/etc/systemd/system/wsl-mount-ext4.service"
Write-Host "Mode : $Mode"
Write-Host "Distribution : $Distribution"
if ($Mode -eq 'install') {
Write-Host "Linux user : $User"
Write-Host "Physical drive : $PhysicalDrive"
Write-Host "Partition : $Partition"
Write-Host "Linux SSD root : $LinuxRealHomeRoot"
}
Write-Host ""
# For install: require systemd to be active
if ($Mode -eq 'install') {
Write-Host "Checking if systemd is active in distro '$Distribution'..." -ForegroundColor Cyan
$systemdOk = Test-SystemdActive -Distribution $Distribution
if (-not $systemdOk) {
Write-Error @"
Systemd does NOT appear to be active in distro '$Distribution'.
Ensure ALL of the following before running this installer:
1) /etc/wsl.conf inside the distro contains:
[boot]
systemd=true
2) You run: wsl --shutdown
3) Start the distro again, and verify inside:
ps -p 1 -o comm=
prints: systemd
Once systemd is active, re-run this script.
"@
exit 1
} else {
Write-Host "Systemd is active (PID 1 is systemd). Continuing..." -ForegroundColor Green
}
}
# -------------------------------
# INSTALL LOGIC
# -------------------------------
if ($Mode -eq 'install') {
# 1. Ensure base directory
if (-not (Test-Path $BaseDir)) {
New-Item -ItemType Directory -Path $BaseDir | Out-Null
}
# 2. Create mount-ext4.ps1 for the scheduled task
$mountScriptContent = @"
param(
[string]\$Drive = "$PhysicalDrive",
[int]\$Partition = $Partition
)
\$EventLogName = "Application"
\$EventSourceName = "WSL-Disk-Tasks-$Distribution"
function Ensure-EventSource {
try {
if (-not [System.Diagnostics.EventLog]::SourceExists(\$EventSourceName)) {
New-EventLog -LogName \$EventLogName -Source \$EventSourceName
}
}
catch {
Write-Host "Warning: Could not create/find event source '\$EventSourceName': \$($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
function Write-Log {
param(
[string]\$Message,
[System.Diagnostics.EventLogEntryType]\$EntryType = [System.Diagnostics.EventLogEntryType]::Information,
[int]\$EventId = 2000
)
Write-Host \$Message
try {
if ([System.Diagnostics.EventLog]::SourceExists(\$EventSourceName)) {
Write-EventLog -LogName \$EventLogName -Source \$EventSourceName -EntryType \$EntryType -EventId \$EventId -Message \$Message
}
}
catch {
Write-Host "Warning: Failed to write to Event Log: \$($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
Ensure-EventSource
Write-Log -Message "[WSL] mount-ext4.ps1 started (drive=\$Drive, partition=\$Partition, distro=$Distribution)." -EventId 2000
Write-Log -Message "[WSL] Running: wsl --mount \$Drive --partition \$Partition --type ext4 --options rw" -EventId 2001
wsl --mount \$Drive --partition \$Partition --type ext4 --options "rw"
\$code = \$LASTEXITCODE
if (\$code -ne 0) {
Write-Log -Message "[WSL] ERROR: wsl --mount failed with exit code \$code." -EntryType Error -EventId 9006
exit \$code
}
try {
\$verifyOutput = wsl -e sh -c "mount | grep '$driveId' || df -h | grep '$driveId'" 2>\$null
if (\$LASTEXITCODE -eq 0 -and \$verifyOutput) {
Write-Log -Message "[WSL] Verification OK: disk appears mounted:`n\$verifyOutput" -EventId 2008
}
else {
Write-Log -Message "[WSL] WARNING: Disk mounted but could not verify via 'mount/df'." -EntryType Warning -EventId 9007
}
}
catch {
Write-Log -Message "[WSL] WARNING: Exception during mount verification: \$($_.Exception.Message)" -EntryType Warning -EventId 9008
}
Write-Log -Message "[WSL] mount-ext4.ps1 finished." -EventId 2010
"@
Set-Content -Path $MountScriptPath -Value $mountScriptContent -Encoding UTF8
Write-Host "Created mount-ext4 script at: $MountScriptPath"
# 3. Register scheduled task (manual trigger only; systemd will call it)
Write-Host "Creating scheduled task '$TaskName'..." -ForegroundColor Cyan
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$MountScriptPath`""
$task = New-ScheduledTask -Action $action
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
Write-Host "Scheduled task '$TaskName' created."
# 4. Create mount-home-ssd.sh inside distro
Write-Host "Creating $LinuxMountScript inside distro..." -ForegroundColor Cyan
$linuxScriptEscaped = @"
cat > $LinuxMountScript << 'EOF'
#!/bin/bash
set -e
USER_NAME="$User"
HOME_DIR="/home/\$USER_NAME"
REAL_HOME_ROOT="$LinuxRealHomeRoot"
REAL_HOME="\$REAL_HOME_ROOT"
# Runs fsck fix once per boot
MAX_WAIT_LOOPS=40 # 40 * 0.5s = 20s
SLEEP_MS=500 # Sleep duration in milliseconds
STATE_FILE="/run/wsl_fsck_done"
LOG_FILE="/var/log/wsl_fsck.log"
# Derived total wait time in seconds (integer math: 40 * 500 / 1000 = 20)
TOTAL_WAIT_SECONDS=\$((MAX_WAIT_LOOPS * SLEEP_MS / 1000))
if [[ -f "\$STATE_FILE" ]]; then
echo "wsl-fsck: already performed this boot; skipping."
else
# Make sure log dir exists
echo "wsl-fsck: ensure log dir exists"
mkdir -p "\$(dirname "\$LOG_FILE")"
# Wait up to TOTAL_WAIT_SECONDS for the SSD mount to appear
for ((i=1; i<=MAX_WAIT_LOOPS; i++)); do
echo "wsl-fsck: attempting mount point in \$REAL_HOME_ROOT (iteration \$i/\$MAX_WAIT_LOOPS)"
if mountpoint -q "\$REAL_HOME_ROOT"; then
# 1. Identify the physical device (e.g., /dev/sdd1)
DEVICE_PATH=\$(findmnt -n -o SOURCE -- "\$REAL_HOME_ROOT" 2>/dev/null || true)
if [[ -z "\$DEVICE_PATH" ]]; then
echo "wsl-fsck: could not determine device for \$REAL_HOME_ROOT" >&2
break
fi
echo "wsl-fsck: found device \$DEVICE_PATH at \$REAL_HOME_ROOT. Preparing to fsck..."
# 2. Aggressively unmount the device, but with a retry limit
UNMOUNT_RETRIES=10
while grep -qsE "^\$DEVICE_PATH " /proc/mounts || \
grep -qsE "^[^ ]+ \$REAL_HOME_ROOT " /proc/mounts; do
echo "wsl-fsck: unmounting \$DEVICE_PATH..."
umount "\$DEVICE_PATH" 2>/dev/null || umount -l "\$DEVICE_PATH" 2>/dev/null || true
sleep 0.5
((UNMOUNT_RETRIES--))
if (( UNMOUNT_RETRIES <= 0 )); then
echo "wsl-fsck: failed to fully unmount \$DEVICE_PATH after multiple attempts" >&2
break
fi
done
# 3. Check filesystem type: only fsck ext4 (or unknown)
FSTYPE=\$(blkid -o value -s TYPE "\$DEVICE_PATH" 2>/dev/null || echo "")
if [[ -n "\$FSTYPE" && "\$FSTYPE" != "ext4" ]]; then
echo "wsl-fsck: \$DEVICE_PATH is type '\$FSTYPE', skipping fsck." >&2
else
echo "--- fsck run at \$(date) ---" >> "\$LOG_FILE"
echo "wsl-fsck: running filesystem check on \$DEVICE_PATH..." | tee -a "\$LOG_FILE"
if ! fsck -y "\$DEVICE_PATH" >> "\$LOG_FILE" 2>&1; then
# fsck non-zero doesn’t always mean failure, but we log it anyway
echo "wsl-fsck: fsck reported non-zero status for \$DEVICE_PATH, see \$LOG_FILE" >&2
fi
fi
# 4. Remount the drive to the original location
echo "wsl-fsck: remounting \$DEVICE_PATH to \$REAL_HOME_ROOT..."
if mount "\$DEVICE_PATH" "\$REAL_HOME_ROOT"; then
echo "wsl-fsck: remount successful."
else
echo "wsl-fsck: remount FAILED for \$DEVICE_PATH -> \$REAL_HOME_ROOT" >&2
fi
# Mark that we ran once this boot
touch "\$STATE_FILE"
echo "wsl-fsck: filesystem repair sequence complete — exiting loop."
break # <<< important: don’t re-run on the remounted drive
else
echo "wsl-fsck: no mountpoint available yet"
fi
echo "wsl-fsck: waiting ..."
# Convert ms -> fractional seconds for sleep (500 -> 0.50)
sleep "0.\$((SLEEP_MS/10))"
done
# If we never saw the mount, you might want a log line:
if ! mountpoint -q "\$REAL_HOME_ROOT"; then
echo "wsl-fsck: \$REAL_HOME_ROOT never appeared within timeout (\${TOTAL_WAIT_SECONDS}s)." >&2
fi
fi
# If still not mounted after waiting, fallback to VHD home
if ! mountpoint -q "\$REAL_HOME_ROOT"; then
exit 0
fi
# Ensure fallback home exists
if [ ! -d "\$HOME_DIR" ]; then
mkdir -p "\$HOME_DIR"
chown "\$USER_NAME:\$USER_NAME" "\$HOME_DIR"
fi
# Skip if already mounted
if mountpoint -q "\$HOME_DIR"; then
exit 0
fi
# Ensure real home exists
if [ ! -d "\$REAL_HOME" ]; then
mkdir -p "\$REAL_HOME"
chown "\$USER_NAME:\$USER_NAME" "\$REAL_HOME"
fi
# Perform bind mount
mount --bind "\$REAL_HOME" "\$HOME_DIR"
EOF
chmod +x $LinuxMountScript
"@
Invoke-WslRoot -Distribution $Distribution -Command $linuxScriptEscaped
# 5. Ensure fallback home + minimal .zshrc
Write-Host "Ensuring fallback /home/$User and minimal .zshrc..." -ForegroundColor Cyan
$fallbackHomeCmd = @"
if [ ! -d /home/$User ]; then
mkdir -p /home/$User
chown $User:$User /home/$User
fi
# Sentinel README
if [ ! -f /home/$User/.FALLBACK_HOME_README ]; then
cat << 'TXT' > /home/$User/.FALLBACK_HOME_README
This is the fallback home directory on the WSL virtual disk.
If your SSD is not mounted, this directory is used temporarily.
TXT
chown $User:$User /home/$User/.FALLBACK_HOME_README
fi
"@
Invoke-WslRoot -Distribution $Distribution -Command $fallbackHomeCmd
# Setup minimal .zshrc only if missing
$zshrcCmd = @"
if [ -d /home/$User ]; then
if [ ! -f /home/$User/.zshrc ]; then
cat << 'EOF' > /home/$User/.zshrc
# Minimal zshrc created by WSL SSD home installer
# You can safely customize this file.
EOF
chown $User:$User /home/$User/.zshrc
fi
fi
"@
Invoke-WslRoot -Distribution $Distribution -Command $zshrcCmd
# 6. systemd service: wsl-mount-ext4.service
Write-Host "Creating systemd units in distro..." -ForegroundColor Cyan
$wslMountService = @"
cat > $WslMountServicePath << 'EOF'
[Unit]
Description=Trigger Windows task to mount PHYSICALDRIVE into WSL
[Service]
Type=oneshot
ExecStart=/mnt/c/Windows/System32/schtasks.exe /run /TN "$TaskName"
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
"@
Invoke-WslRoot -Distribution $Distribution -Command $wslMountService
# 7. systemd service: home-<user>-ssd.service
$homeService = @"
cat > $HomeServicePath << 'EOF'
[Unit]
Description=Bind-mount SSD-backed home for user $User
After=wsl-mount-ext4.service
Requires=wsl-mount-ext4.service
[Service]
Type=oneshot
ExecStart=$LinuxMountScript
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
"@
Invoke-WslRoot -Distribution $Distribution -Command $homeService
# 8. Enable the services
$enableServicesCmd = @"
systemctl daemon-reload
systemctl enable wsl-mount-ext4.service
systemctl enable $HomeServiceName
"@
Invoke-WslRoot -Distribution $Distribution -Command $enableServicesCmd
Write-Host ""
Write-Host "INSTALL COMPLETE." -ForegroundColor Green
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " 1) Run: wsl --shutdown"
Write-Host " 2) Start your distro '$Distribution' again."
Write-Host "WSL boot will:"
Write-Host " - trigger the scheduled task '$TaskName' to run mount-ext4.ps1"
Write-Host " - mount $PhysicalDrive partition $Partition into WSL"
Write-Host " - systemd will bind-mount $LinuxRealHomeRoot onto /home/$User"
}
# -------------------------------
# UNINSTALL LOGIC
# -------------------------------
if ($Mode -eq 'uninstall') {
Write-Host "Uninstalling WSL SSD home integration for distro '$Distribution'..." -ForegroundColor Cyan
# 1. Remove systemd units + helper script
$removeLinuxCmd = @"
if [ -f $HomeServicePath ]; then
systemctl disable $(basename $HomeServicePath) 2>/dev/null || true
rm -f $HomeServicePath
fi
if [ -f $WslMountServicePath ]; then
systemctl disable $(basename $WslMountServicePath) 2>/dev/null || true
rm -f $WslMountServicePath
fi
if [ -f $LinuxMountScript ]; then
rm -f $LinuxMountScript
fi
systemctl daemon-reload
"@
Invoke-WslRoot -Distribution $Distribution -Command $removeLinuxCmd
# 2. Remove scheduled task
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
Write-Host "Removing scheduled task '$TaskName'..." -ForegroundColor Cyan
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
}
# 3. Remove script directory
if (Test-Path $BaseDir) {
Write-Host "Removing base directory: $BaseDir" -ForegroundColor Cyan
Remove-Item -Path $BaseDir -Recurse -Force
}
Write-Host "UNINSTALL COMPLETE." -ForegroundColor Green
Write-Host "You may want to run: wsl --shutdown before restarting the distro."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment