Skip to content
WSL2 Cheat/sheet

All commands

Every command and config file, grouped by recipe. For when you know what you want; follow the recipe link for prerequisites and how to check it worked.

Set up a new machineStart here

PowerShell · AdminInstall WSL with Ubuntu
wsl --install -d Ubuntu
PowerShellUpdate WSL and check the version
wsl --update
wsl -l -v      # Ubuntu, VERSION 2
WSLUpgrade packages and add build tools
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y build-essential git curl unzip
WSLYour code lives here
mkdir -p ~/dev
cd ~/dev
WSLOpen the folder in VS Code, running against Linux
cd ~/dev
code .
WSLInstall mise (bash, Ubuntu's default shell)
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
exec bash
mise use -g node@lts      # or python@3.13, go, java… whatever your project needs
WSLGit identity and defaults
git config --global user.name  "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global core.autocrlf input
WSLA test project
cd ~/dev
npm create vite@latest hello -- --template vanilla
cd hello && npm install && npm run dev

Open a WSL project in your editorDaily development

WSLOpen the current folder
cd ~/dev/my-app
code .        # or: cursor .

Install Node, Python and other runtimes with miseDaily development

WSLInstall mise (bash)
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
exec bash
WSLA typical web and mobile toolchain
mise use -g node@lts pnpm@latest bun@latest python@3.13 uv@latest
mise settings add idiomatic_version_file_enable_tools node   # respect .nvmrc / .node-version
WSLDaily use
mise use node@22        # pin for THIS project
mise install            # install what the project asks for
mise ls                 # what's installed and active
mise up                 # upgrade everything
WSLRuntimes come from mise, not Windows or apt
which node        # ~/.local/share/mise/installs/node/...
node -v
cd ~/dev/my-app && mise current   # versions the project pins
WSLRemove mise and everything it installed
mise implode      # removes mise and its installs

Connect Git to GitHub with SSH and signed commitsDaily development

WSLInstall the GitHub CLI
sudo apt install -y gh
WSLA key for this machine
ssh-keygen -t ed25519 -C "you@example.com" -f ~/.ssh/id_ed25519
WSLSign commits and tags with SSH
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
echo "you@example.com $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
WSLUpload the key (auth + signing)
gh auth login                     # GitHub.com → SSH → skip key upload
gh auth refresh -s admin:public_key,admin:ssh_signing_key
gh ssh-key add ~/.ssh/id_ed25519.pub --title "$(hostname) WSL" --type authentication
gh ssh-key add ~/.ssh/id_ed25519.pub --title "$(hostname) WSL signing" --type signing
WSLPull, rebase and push defaults
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --global push.autoSetupRemote true
WSLAuth and signing work
ssh -T git@github.com            # "Hi <user>! You've successfully authenticated"
git log --show-signature -1      # after your next commit: "Good \"git\" signature"

Install Docker in WSLDaily development

WSLIs systemd running?
ps -p 1 -o comm=      # prints: systemd
WSLInstall Docker Engine
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER     # then close and reopen the terminal
File/etc/docker/daemon.json (sudo)
{
  "log-driver": "local",
  "log-opts": { "max-size": "20m", "max-file": "3" },
  "features": { "buildkit": true, "containerd-snapshotter": true },
  "builder": { "gc": { "enabled": true, "defaultKeepStorage": "30GB" } }
}
WSLApply the daemon config
sudo systemctl restart docker
WSLDocker works without sudo
docker run --rm hello-world
docker compose version
docker info --format '{{.LoggingDriver}}'    # local
WSLRemove Docker Engine and its data
sudo apt purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo rm -rf /var/lib/docker /var/lib/containerd /etc/docker/daemon.json

Run shared Postgres and Redis for all projectsDaily development

File~/dev/infra/docker-compose.yml
name: devinfra
services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment: { POSTGRES_USER: dev, POSTGRES_PASSWORD: dev, POSTGRES_DB: dev }
    ports: ["127.0.0.1:5432:5432"]
    volumes: [pgdata:/var/lib/postgresql/data]
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    ports: ["127.0.0.1:6379:6379"]
    volumes: [redisdata:/data]
volumes: { pgdata: {}, redisdata: {} }
WSLStart the shared services
echo 'alias devinfra="docker compose -f ~/dev/infra/docker-compose.yml"' >> ~/.bashrc
source ~/.bashrc
devinfra up -d
WSLBoth services answer
devinfra ps                                  # both "running"
devinfra exec postgres pg_isready -U dev     # accepting connections
devinfra exec redis redis-cli ping           # PONG
WSLStop and remove (-v also deletes the data)
devinfra down        # keep data
devinfra down -v     # delete volumes too

Connect a container to a service in WSLDaily development

Filedocker-compose.yml
services:
  worker:
    image: my-worker
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      API_URL: http://host.docker.internal:3000
WSLSame thing with docker run
docker run --rm --add-host=host.docker.internal:host-gateway curlimages/curl \
  -s http://host.docker.internal:3000/health
WSLCall the WSL service from inside a container
docker compose exec worker sh -c 'wget -qO- http://host.docker.internal:3000/health || curl -s http://host.docker.internal:3000/health'

Turn on mirrored networkingDaily development

File%USERPROFILE%\.wslconfig
[wsl2]
networkingMode=mirrored    # WSL shares Windows' IPs; localhost works both ways
dnsTunneling=true          # fixes DNS on VPNs and odd networks
firewall=true              # Windows firewall rules apply to WSL
autoProxy=true             # use the Windows proxy settings

[experimental]
hostAddressLoopback=true   # Windows can reach services bound to WSL's LAN IP
PowerShellApply .wslconfig
wsl --shutdown     # wait ~8 s, then open Ubuntu again
WSLSame IPs as Windows
ip -br addr           # your Windows LAN IP (e.g. 192.168.1.50) appears here

Open a WSL port to your networkDaily development

PowerShell · AdminAllow inbound TCP 8081 to WSL
New-NetFirewallHyperVRule -Name "WSL-8081" -DisplayName "WSL dev 8081" `
  -Direction Inbound -VMCreatorId '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' `
  -Protocol TCP -LocalPorts 8081
PowerShell · AdminNAT mode: forward 8081 to WSL and allow it
$ip = (wsl hostname -I).Trim().Split(' ')[0]
netsh interface portproxy add v4tov4 listenport=8081 listenaddress=0.0.0.0 connectport=8081 connectaddress=$ip
New-NetFirewallRule -DisplayName "WSL dev 8081" -Direction Inbound -Protocol TCP -LocalPort 8081 -Action Allow
PowerShellThe rule exists
Get-NetFirewallHyperVRule -Name "WSL-8081" | Select-Object Name, Enabled, LocalPorts
PowerShell · AdminRemove the rule
Remove-NetFirewallHyperVRule -Name "WSL-8081"

Test a local HTTPS site on your phoneDaily development

WSLInstall mkcert and its CA
sudo apt install -y mkcert libnss3-tools
mkcert -install          # trusts the CA inside Linux
mkcert -CAROOT           # folder holding rootCA.pem
WSLCertificate for localhost and the LAN IP
mkdir -p ~/dev/certs && cd ~/dev/certs
mkcert -cert-file dev.pem -key-file dev-key.pem localhost 127.0.0.1 ::1 192.168.1.50
Filevite.config.ts
import fs from 'node:fs';
import os from 'node:os';
import { defineConfig } from 'vite';

const certs = `${os.homedir()}/dev/certs`;
export default defineConfig({
  server: {
    host: true,                   // listen on all interfaces
    https: { key: fs.readFileSync(`${certs}/dev-key.pem`), cert: fs.readFileSync(`${certs}/dev.pem`) },
  },
});
WSLCopy the CA to Windows
cp "$(mkcert -CAROOT)/rootCA.pem" /mnt/c/Users/<you>/Downloads/mkcert-rootCA.pem
PowerShellTrust it for your Windows user
certutil -user -addstore Root "$env:USERPROFILE\Downloads\mkcert-rootCA.pem"

Develop for Android with the emulator on WindowsDaily development

WSLadb client
sudo apt install -y adb
WSLUse the Windows adb server (bash)
echo 'export ADB_SERVER_SOCKET=tcp:127.0.0.1:5037' >> ~/.bashrc
source ~/.bashrc
PowerShellWindows: adb server on all interfaces
adb kill-server
adb -a nodaemon server start
WSLWSL: use the Windows host's IP
export ADB_SERVER_SOCKET=tcp:$(ip route show default | awk '{print $3}'):5037
WSLWSL sees the emulator
adb devices         # emulator-5554   device

Open your Expo app on a phoneDaily development

WSLStart the dev server
cd ~/dev/my-app
npx expo start

Debug a TypeScript applicationDaily development

WSLAdd tsx
pnpm add -D tsx      # or: npm i -D tsx
WSLRun as usual inside the Debug Terminal
pnpm dev          # or: npx tsx src/index.ts, pnpm test
File.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Run current TS file",
      "runtimeExecutable": "node",
      "runtimeArgs": ["--import", "tsx"],
      "program": "${file}",
      "skipFiles": ["<node_internals>/**"],
      "console": "integratedTerminal"
    }
  ]
}
WSLWait for a debugger before running
node --inspect-brk --import tsx src/index.ts

Back up and restore a development databaseDaily development

WSLBack up one database
mkdir -p ~/dev/backups
devinfra exec -T postgres pg_dump -U dev -Fc my_app > ~/dev/backups/my_app-$(date +%F_%H%M).dump
WSLRestore (drops and recreates objects in my_app)
devinfra exec -T postgres pg_restore -U dev -d my_app --clean --if-exists --no-owner < ~/dev/backups/my_app-2026-10-06_1430.dump
WSLCopy backups to Windows
cp ~/dev/backups/*.dump /mnt/c/Users/<you>/Backups/
WSLThe dump is readable and the data is back
devinfra exec -T postgres pg_restore -l < ~/dev/backups/my_app-2026-10-06_1430.dump | head   # table of contents
devinfra exec postgres psql -U dev -d my_app -c '\dt'                                    # tables after restore

Raise file-watcher limits for large reposDaily development

File/etc/sysctl.d/99-dev.conf (sudo)
fs.inotify.max_user_watches=524288   # files watched, across all watchers
fs.inotify.max_user_instances=1024   # separate watcher processes
vm.max_map_count=262144              # only if you run Elasticsearch/OpenSearch
WSLLoad the new limits
sudo sysctl --system
WSLThe limits are active
cat /proc/sys/fs/inotify/max_user_watches     # 524288
cat /proc/sys/fs/inotify/max_user_instances   # 1024
WSLBack to the defaults
sudo rm /etc/sysctl.d/99-dev.conf

Raise open-file limitsDaily development

WSLShells and systemd services
printf '*  soft nofile 65536\n*  hard nofile 524288\n' | sudo tee /etc/security/limits.d/99-dev.conf
sudo mkdir -p /etc/systemd/system.conf.d
printf '[Manager]\nDefaultLimitNOFILE=65536:524288\n' | sudo tee /etc/systemd/system.conf.d/99-nofile.conf
WSLSystem-wide maximum
echo 'fs.file-max=2097152' | sudo tee /etc/sysctl.d/99-file-max.conf
PowerShellRestart WSL
wsl --shutdown
WSLNew limits in a fresh shell and for Docker
ulimit -n                                  # 65536
systemctl show docker -p LimitNOFILE       # if Docker is installed
WSLRemove the overrides
sudo rm /etc/security/limits.d/99-dev.conf /etc/systemd/system.conf.d/99-nofile.conf /etc/sysctl.d/99-file-max.conf

Speed up installs with Defender exclusionsDaily development

PowerShell · AdminExclude the WSL disk and processes
$paths = @("$env:LOCALAPPDATA\wsl") +
  (Resolve-Path "$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.Ubuntu*\LocalState" -EA 0).Path
$procs = "vmmemWSL", "vmmem", "wslhost.exe", "wslservice.exe"

$paths | ? { $_ } | % { Add-MpPreference -ExclusionPath $_ }
$procs | % { Add-MpPreference -ExclusionProcess $_ }
PowerShell · AdminList the exclusions
(Get-MpPreference).ExclusionPath
(Get-MpPreference).ExclusionProcess
PowerShell · AdminRemove the exclusions
$paths = @("$env:LOCALAPPDATA\wsl") +
  (Resolve-Path "$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.Ubuntu*\LocalState" -EA 0).Path
$paths | ? { $_ } | % { Remove-MpPreference -ExclusionPath $_ }
"vmmemWSL", "vmmem", "wslhost.exe", "wslservice.exe" | % { Remove-MpPreference -ExclusionProcess $_ }

Cap WSL memory and CPUDaily development

File%USERPROFILE%\.wslconfig
[wsl2]
memory=16GB                # ~50–75% of your RAM
processors=8               # all threads, or all minus 2
swap=8GB                   # ~25–50% of memory

[experimental]
autoMemoryReclaim=dropcache  # give idle RAM back to Windows
PowerShellApply .wslconfig
wsl --shutdown     # wait ~8 s, then reopen Ubuntu
WSLWSL sees the new limits
free -h      # "total" ≈ your memory= value
nproc        # = processors=

Shrink the WSL virtual diskDaily development

WSLSee and reclaim the big items
df -h /                                   # used vs size inside Linux
docker system df                          # images, build cache, volumes
docker system prune                       # unused containers, networks, dangling images
docker builder prune                      # build cache
PowerShellMake the distro's disk sparse
wsl --shutdown
wsl --manage Ubuntu --set-sparse true
WSLWeekly TRIM, plus one now
sudo systemctl enable --now fstrim.timer
sudo fstrim -av
PowerShellSize of the virtual disk file
Get-ChildItem "$env:LOCALAPPDATA\wsl", "$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.*" -Recurse -Filter ext4.vhdx -EA 0 |
  Select-Object FullName, @{n='GB';e={[math]::Round($_.Length/1GB,1)}}
PowerShellTurn sparse mode off
wsl --shutdown
wsl --manage Ubuntu --set-sparse false

Keep WSL, packages and runtimes up to dateDaily development

PowerShellWSL itself
wsl --update          # new WSL + kernel
wsl --version
wsl -l -v             # distros and their state
wsl --shutdown        # restart into the new version
WSLPackages, runtimes, global CLIs
sudo apt update && sudo apt full-upgrade      # system, docker, gh
mise up                                      # runtimes and CLIs from mise
npm i -g wrangler@latest eas-cli@latest      # global npm CLIs, if you use them
WSLDisk check
docker system df          # reclaim with: docker system prune
du -xh --max-depth=1 ~ 2>/dev/null | sort -h | tail

Back up and restore your WSL distroDaily development

PowerShellSnapshot (stops the distro)
$dir = "$env:USERPROFILE\WSL-Backups"; mkdir $dir -Force | Out-Null
wsl --terminate Ubuntu
wsl --export Ubuntu "$dir\Ubuntu-$(Get-Date -f yyyy-MM-dd_HHmm).tar.gz" --format tar.gz
PowerShellRestore as a second distro, to test or rescue files
wsl --import Ubuntu-Restored C:\WSL\Ubuntu-Restored "$env:USERPROFILE\WSL-Backups\Ubuntu-<date>.tar.gz"
wsl -d Ubuntu-Restored
PowerShellSnapshot exists and imports
Get-ChildItem "$env:USERPROFILE\WSL-Backups" | Sort-Object LastWriteTime | Select-Object -Last 3 Name, Length
wsl -l -v       # Ubuntu-Restored appears after an import
PowerShellRemove the restored copy
wsl --unregister Ubuntu-Restored

Phone cannot reach the dev serverFix a problem

WSLBind address of the dev server
ss -ltn 'sport = :5173'      # 0.0.0.0 or [::] = reachable; 127.0.0.1 = this machine only
WSLMirrored mode shows the same IPs as Windows
wslinfo --networking-mode       # recent WSL; otherwise compare `ip -br addr` with Windows ipconfig
PowerShellThe PC's LAN address
ipconfig | Select-String IPv4
PowerShellInbound rules for WSL
Get-NetFirewallHyperVRule | Where-Object Direction -eq Inbound |
  Select-Object Name, DisplayName, LocalPorts, Action

A port is already in useFix a problem

WSLWho listens on port 3000 in Linux
ss -ltnp 'sport = :3000'
sudo lsof -iTCP:3000 -sTCP:LISTEN     # shows processes of other users too
WSLContainers publishing port 3000
docker ps --filter publish=3000
PowerShellWho listens on port 3000 in Windows
Get-NetTCPConnection -LocalPort 3000 -State Listen -EA 0 |
  Select-Object LocalAddress, OwningProcess, @{n='Process';e={(Get-Process -Id $_.OwningProcess).ProcessName}}
PowerShellPort ranges reserved by Windows (Hyper-V, WinNAT)
netsh interface ipv4 show excludedportrange protocol=tcp
WSLStop the process
kill <pid>          # polite; use kill -9 <pid> only if it ignores this
PowerShell · AdminRestart WinNAT (WSL and Docker shut down first)
wsl --shutdown
net stop winnat
net start winnat

DNS fails after connecting to a VPNFix a problem

WSLName vs IP
getent hosts github.com            # no output = DNS fails
curl -sI https://1.1.1.1 | head -1 # a response = the network itself works
PowerShellWindows resolver
Resolve-DnsName github.com
WSLCurrent resolver config
cat /etc/resolv.conf
grep -iE 'dnsTunneling|autoProxy|networkingMode' /mnt/c/Users/*/.wslconfig 2>/dev/null
File%USERPROFILE%\.wslconfig
[wsl2]
dnsTunneling=true
autoProxy=true        # also pick up the VPN's proxy settings
File/etc/wsl.conf (sudo)
[network]
generateResolvConf=false
WSLAfter wsl --shutdown and reopening
sudo rm /etc/resolv.conf      # it is a symlink WSL created
printf 'nameserver 172.16.0.53\nnameserver 1.1.1.1\n' | sudo tee /etc/resolv.conf

Docker cannot reach my local APIFix a problem

WSLEngine or Desktop
docker info --format '{{.OperatingSystem}}'   # "Docker Desktop" or your Ubuntu version
WSLAPI bind address
ss -ltn 'sport = :4000'     # 127.0.0.1 = WSL only; 0.0.0.0 or [::] = also containers
WSLTest from inside the container
docker compose exec app getent hosts host.docker.internal
docker compose exec app wget -qO- http://host.docker.internal:4000/health   # or curl, if the image has it

adb devices shows nothing in WSLFix a problem

PowerShellWindows adb
adb devices
WSLWSL adb settings
echo $ADB_SERVER_SOCKET          # expect tcp:127.0.0.1:5037
adb version
adb devices

Changes do not trigger hot reloadFix a problem

WSLProject location
pwd          # starts with /mnt/c/… = on the Windows drive
WSLWatcher limits
cat /proc/sys/fs/inotify/max_user_watches /proc/sys/fs/inotify/max_user_instances
WSLClone again on the Linux disk
mkdir -p ~/dev && cd ~/dev
git clone <repo-url>

Builds and installs are slowFix a problem

WSLLocation and tool paths
pwd                       # /mnt/c/… = Windows drive
which node npm pnpm git   # any /mnt/c/… path = a Windows binary
WSLMemory and swap while the build runs
free -h
vmstat 2 5                # non-zero "si"/"so" columns = swapping
WSLBaseline
time pnpm install --frozen-lockfile

Too many open filesFix a problem

WSLSoft and hard limits
ulimit -Sn; ulimit -Hn
WSLLimit of a running service
systemctl show docker -p LimitNOFILE
WSLOpen files of a process
ls /proc/<pid>/fd | wc -l

WSL uses too much memoryFix a problem

WSLUsed vs cache
free -h     # "used" = processes; "buff/cache" = file cache Linux can drop
WSLTop memory users
ps aux --sort=-%mem | head -n 8
docker stats --no-stream 2>/dev/null
PowerShellCurrent WSL config
Get-Content $env:USERPROFILE\.wslconfig -EA 0

Windows disk space is running outFix a problem

PowerShellFind ext4.vhdx files and their size
Get-ChildItem "$env:LOCALAPPDATA\wsl", "$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.*" -Recurse -Filter ext4.vhdx -EA 0 |
  Select-Object FullName, @{n='GB';e={[math]::Round($_.Length/1GB,1)}}
WSLUsed space inside the distro
df -h /
WSLBiggest folders and Docker usage
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -n 8
du -h --max-depth=1 ~ 2>/dev/null | sort -h | tail -n 8
docker system df

Not sure what is wrong? Run a health checkFix a problem

PowerShellVersion and distro state
wsl --version          # WSL and kernel versions
wsl -l -v              # distros, state, and VERSION 2
WSLHealth check
free -h && nproc                  # matches memory= and processors= in .wslconfig?
systemctl is-system-running       # "running", not "degraded"
systemctl --failed
ip -br addr                       # mirrored mode: same IPs as Windows
getent hosts github.com           # DNS works
docker ps                         # works without sudo (if you use Docker)

.wslconfig changes do nothingFix a problem

PowerShellExact file name and location
Get-ChildItem $env:USERPROFILE -Force -Filter '.wslconfig*' | Select-Object Name, Length, LastWriteTime
PowerShellRunning distros
wsl -l --running
PowerShellApply .wslconfig
wsl --shutdown

systemctl says degradedFix a problem

WSLWhich units failed, and why
systemctl --failed
journalctl -b -u systemd-binfmt.service --no-pager | tail -n 5   # if it is listed
WSLStop systemd-binfmt from starting
sudo systemctl mask systemd-binfmt.service

Clock is wrong after sleepFix a problem

WSLLinux time vs Windows time
date
powershell.exe -NoProfile -Command Get-Date 2>/dev/null || /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -Command Get-Date
WSLResync the clock
sudo hwclock -s || sudo systemctl restart systemd-timesyncd

GPU or CUDA not foundFix a problem

WSLDoes WSL see the Windows driver?
ls /usr/lib/wsl/lib/ | grep -i -E 'cuda|nvidia'
/usr/lib/wsl/lib/nvidia-smi
dpkg -l | grep -i -E 'nvidia-driver|nvidia-dkms'     # should print nothing

code, explorer.exe or powershell.exe not foundFix a problem

WSLIs interop on, and is the Windows PATH appended?
grep -A3 '^\[interop\]' /etc/wsl.conf
ls /mnt/c/Windows/System32/cmd.exe
cat /proc/sys/fs/binfmt_misc/WSLInterop 2>/dev/null | head -n 1   # "enabled" = Windows .exe files can run

node or npm is the Windows oneFix a problem

WSLWhich binaries win on PATH
which -a node npm npx pnpm
echo "$PATH" | tr ':' '\n' | grep -n -E 'mise|nvm|/mnt/c'

sudo -E fails or is ignoredFix a problem

WSLWhich sudo is this?
sudo --version | head -n 1      # "sudo-rs …" or "Sudo version 1.9…"
WSLSet variables for one sudo command
sudo env HTTPS_PROXY="$HTTPS_PROXY" FOO=bar some-command

Every file shows as changedFix a problem

WSLWhat actually changed
git diff --stat | tail -n 3
git diff | grep -c $'\r'             # > 0 = CRLF in the changes
git config --show-origin --get-all core.autocrlf
git config --get core.filemode
pwd                                  # /mnt/c/… = repo on the Windows drive
WSLKeep LF everywhere
git config --global core.autocrlf input
printf '* text=auto eol=lf\n' >> .gitattributes
git add --renormalize .

.wslconfig settingsConfiguration reference

File%USERPROFILE%\.wslconfig
# Applies after: wsl --shutdown   (wait ~8 s, then reopen a terminal)
[wsl2]
memory=16GB                # ~50–75% of your RAM
processors=8               # all threads, or all minus 2
swap=8GB                   # ~25–50% of memory
networkingMode=mirrored    # WSL shares Windows' IPs; localhost works both ways
dnsTunneling=true          # fixes DNS on VPNs and odd networks
firewall=true              # Windows firewall rules apply to WSL
autoProxy=true             # use the Windows proxy settings
guiApplications=true       # WSLg: run Linux GUI apps
nestedVirtualization=true  # KVM / Android emulators inside WSL

[experimental]
autoMemoryReclaim=dropcache  # give idle RAM back to Windows
sparseVhd=true               # virtual disk shrinks when you delete files
hostAddressLoopback=true     # Windows can reach services bound to WSL's LAN IP

/etc/wsl.conf settingsConfiguration reference

WSLBack up before editing
sudo cp /etc/wsl.conf /etc/wsl.conf.bak
File/etc/wsl.conf (sudo)
[boot]
systemd=true

[user]
default=yourname           # also used when you restore a backup

[interop]
enabled=true
appendWindowsPath=true     # false = faster shell, but code/explorer.exe disappear

[automount]
enabled=true
options="metadata,umask=22,fmask=11"   # real Linux permissions on /mnt/c

[network]
generateResolvConf=true    # let WSL manage /etc/resolv.conf

Where files liveConfiguration reference

WSL~/dev
~/dev/
├── projects/    # repos, optionally projects/<org>/<repo>
├── infra/       # shared docker-compose (Postgres, Redis…)
└── sandbox/     # throwaway experiments
WSLCopy something in from Windows
cp /mnt/c/Users/<you>/Downloads/x.zip ~/dev/sandbox/

Networking modesConfiguration reference

WSLWhat's listening, and on which interface?
ss -tlnp                     # 0.0.0.0 / [::] = reachable from LAN; 127.0.0.1 = local only

Switch your shell to zshOptional extras

WSLzsh and plugins
sudo apt install -y zsh zsh-autosuggestions zsh-syntax-highlighting
File~/.zshrc
autoload -Uz compinit && compinit -C
eval "$(~/.local/bin/mise activate zsh)"
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh   # must be last
WSLChange the default shell
chsh -s "$(which zsh)"
WSLBack to bash
chsh -s /bin/bash

A fast prompt with StarshipOptional extras

WSLInstall Starship
mkdir -p ~/.local/bin
curl -sS https://starship.rs/install.sh | sh -s -- -b ~/.local/bin
File~/.zshrc (or ~/.bashrc with: starship init bash)
eval "$(starship init zsh)"

Modern command-line toolsOptional extras

WSLFrom apt
sudo apt install -y fzf zoxide direnv eza bat fd-find ripgrep jq btop tmux git-delta
WSLFrom mise
mise use -g lazygit atuin dust lazydocker
File~/.zshrc: init order
autoload -Uz compinit && compinit -C
eval "$(~/.local/bin/mise activate zsh)"
eval "$(starship init zsh)"          # if you use the prompt
eval "$(direnv hook zsh)"
source /usr/share/doc/fzf/examples/key-bindings.zsh
eval "$(atuin init zsh --disable-up-arrow)"
eval "$(zoxide init zsh --cmd cd)"   # late: after other cd/chpwd hooks
alias bat=batcat fd=fdfind           # Ubuntu package names
alias ll="eza -la --git --group-directories-first"
alias lg=lazygit
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh   # must be last
WSLdelta as Git's pager
git config --global core.pager delta
git config --global delta.navigate true

Call Windows apps without the Windows PATHOptional extras

File/etc/wsl.conf (sudo)
[interop]
enabled=true
appendWindowsPath=false
File~/.config/shell/common.sh
_win=/mnt/c/Windows
_winuser=$(wslpath "$($_win/System32/cmd.exe /c 'echo %USERPROFILE%' 2>/dev/null | tr -d '\r')")
_apps="$_winuser/AppData/Local/Programs"
[ -d "$_apps/Microsoft VS Code/bin" ] && export PATH="$PATH:$_apps/Microsoft VS Code/bin"
[ -d "$_apps/cursor/resources/app/bin" ] && export PATH="$PATH:$_apps/cursor/resources/app/bin"

alias open="$_win/explorer.exe"                 # open .  → Explorer here
alias clip="$_win/System32/clip.exe"            # echo hi | clip
alias pwsh="$_win/System32/WindowsPowerShell/v1.0/powershell.exe"
alias winget="$_winuser/AppData/Local/Microsoft/WindowsApps/winget.exe"
unset _win _winuser _apps
WSLLoad it in bash (use ~/.zshrc for zsh)
mkdir -p ~/.config/shell
echo 'source ~/.config/shell/common.sh' >> ~/.bashrc

Make Windows Terminal open in LinuxOptional extras

PowerShellUbuntu profile command line
wsl.exe -d Ubuntu --cd ~

Small conveniencesOptional extras

WSLwslview as the default browser
sudo apt install -y wslu
echo 'export BROWSER=wslview' >> ~/.bashrc      # or ~/.zshrc