Windows Setup Guide (WSL2)
Sandkasten requires Linux kernel features (namespaces, cgroups v2, overlayfs). On Windows, use WSL2.
Prerequisites
- Windows 10 (version 2004+) or Windows 11
- WSL2 enabled
- Ubuntu 22.04+ (or any Linux distro with cgroups v2)
Step 1: Install WSL2
Open PowerShell as Administrator:
# Enable WSL
wsl --install
# Or install specific distro
wsl --install -d Ubuntu-22.04
Restart if prompted, then complete Ubuntu setup (username, password).
Step 2: Install Dependencies in WSL2
Open WSL2:
wsl
Inside WSL2:
# Update packages
sudo apt update && sudo apt upgrade -y
# Install Go 1.24+
sudo apt install -y golang-go git curl
# Verify Go version (need 1.24+)
go version
# If Go is too old, install manually:
# wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
# sudo tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
# echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
# source ~/.bashrc
Install Task (build tool):
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
echo 'export PATH=$PATH:$HOME/.local/bin' >> ~/.bashrc
source ~/.bashrc
Step 3: Clone and Build
Important
~/sandkasten), not on /mnt/c/. Building on NTFS can cause issues.cd ~
git clone https://github.com/p-arndt/sandkasten
cd sandkasten
task build
This creates:
bin/runner- Runner binary (PID 1 in sandboxes)bin/sandkasten- The daemonbin/imgbuilder- Image management tool
Step 4: Create an Image
You need at least one rootfs image. Preferred: pull from a registry (no Docker daemon required):
# Pull from registry (recommended)
sudo ./bin/sandkasten image pull --name python python:3.12-slim
./bin/sandkasten image list
Alternatively, export from Docker and import with imgbuilder:
docker create --name temp python:3.12-slim
docker export temp | gzip > /tmp/python.tar.gz
docker rm temp
sudo ./bin/imgbuilder import --name python --tar /tmp/python.tar.gz
Step 5: Create Configuration
cat > sandkasten.yaml << 'EOF'
listen: "127.0.0.1:8080"
api_key: "sk-sandbox-test"
data_dir: "/var/lib/sandkasten"
default_image: "python"
defaults:
cpu_limit: 1.0
mem_limit_mb: 512
pids_limit: 256
network_mode: "none"
EOF
Step 6: Create Data Directories
Important
/var/lib/sandkasten). Do not use /mnt/c/...—NTFS does not support overlayfs.sudo mkdir -p /var/lib/sandkasten/{images,sessions,workspaces}
Step 7: Start the Daemon
You can start the daemon either inside WSL (see below) or from Windows using the sandkasten-wsl helper.
Option A: From inside WSL
# Run with sudo (required for namespaces)
# Foreground (logs in terminal):
sudo ./bin/sandkasten --config sandkasten.yaml
# Or in background (like Docker daemon):
sudo ./bin/sandkasten daemon -d --config sandkasten.yaml
Option B: From Windows (sandkasten-wsl helper)
The sandkasten-wsl executable lets you start, check, and stop the daemon from Windows PowerShell without opening a WSL shell. Build it (from any OS) with:
task sandkasten-wsl
# or: GOOS=windows go build -o bin/sandkasten-wsl.exe ./cmd/sandkasten-wsl
Then on Windows, ensure the Linux binary and config exist inside WSL (e.g. you built and configured Sandkasten in WSL as in Steps 3–6). From PowerShell:
# Start daemon in default WSL distro
.\bin\sandkasten-wsl.exe start
# Use a specific distro and config
.\bin\sandkasten-wsl.exe start --distro Ubuntu-22.04 --config ~/sandkasten.yaml
# Check if daemon is running
.\bin\sandkasten-wsl.exe status
# Stop daemon
.\bin\sandkasten-wsl.exe stop
The helper runs wsl -d <distro> -- sudo sandkasten daemon -d ... for you. The sandkasten binary must be on the PATH inside that WSL distro, or pass --binary /path/to/sandkasten.
To list sessions from WSL: ./bin/sandkasten ps
You should see (when running in foreground):
INFO listening addr=127.0.0.1:8080
sandkasten daemon ready
API: http://127.0.0.1:8080/v1
Step 8: Test from WSL2
Open a new terminal and run:
# Check health
curl http://localhost:8080/healthz
# {"status":"ok"}
# Create a session
curl -X POST http://localhost:8080/v1/sessions \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"image":"python"}'
# {"id":"abc123def456","image":"python","status":"running",...}
# Save the session ID
SESSION_ID="abc123def456" # Use the ID from above
Execute Commands
# Simple command
curl -X POST http://localhost:8080/v1/sessions/$SESSION_ID/exec \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"cmd":"echo hello world"}'
# Response:
# {"exit_code":0,"cwd":"/workspace","output":"hello world\n","truncated":false,"duration_ms":42}
# Run Python
curl -X POST http://localhost:8080/v1/sessions/$SESSION_ID/exec \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"cmd":"python3 -c \"print(2 + 2)\""}'
# Response:
# {"exit_code":0,"cwd":"/workspace","output":"4\n",...}
# Install package and use it
curl -X POST http://localhost:8080/v1/sessions/$SESSION_ID/exec \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"cmd":"pip install requests -q && python3 -c \"import requests; print(requests.__version__)\""}'
Write and Read Files
# Write a file
curl -X POST http://localhost:8080/v1/sessions/$SESSION_ID/fs/write \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"path":"/workspace/hello.py","text":"print(\"Hello from file!\")"}'
# Read it back
curl "http://localhost:8080/v1/sessions/$SESSION_ID/fs/read?path=/workspace/hello.py" \
-H "Authorization: Bearer sk-sandbox-test"
# Run it
curl -X POST http://localhost:8080/v1/sessions/$SESSION_ID/exec \
-H "Authorization: Bearer sk-sandbox-test" \
-H "Content-Type: application/json" \
-d '{"cmd":"python3 /workspace/hello.py"}'
Cleanup
# Destroy session
curl -X DELETE http://localhost:8080/v1/sessions/$SESSION_ID \
-H "Authorization: Bearer sk-sandbox-test"
Step 9: Access from Windows
The daemon running in WSL2 is accessible from Windows via localhost:
PowerShell
# Check health
curl http://localhost:8080/healthz
# Create session
$session = Invoke-RestMethod -Uri "http://localhost:8080/v1/sessions" `
-Method POST `
-Headers @{ "Authorization" = "Bearer sk-sandbox-test" } `
-ContentType "application/json" `
-Body '{"image":"python"}'
$sessionId = $session.id
Write-Host "Session ID: $sessionId"
# Execute command
$result = Invoke-RestMethod -Uri "http://localhost:8080/v1/sessions/$sessionId/exec" `
-Method POST `
-Headers @{ "Authorization" = "Bearer sk-sandbox-test" } `
-ContentType "application/json" `
-Body '{"cmd":"python3 -c \"print(42)\""}'
Write-Host "Output: $($result.output)"
Python (Windows)
pip install sandkasten
import asyncio
from sandkasten import SandboxClient
async def main():
client = SandboxClient(
base_url="http://localhost:8080",
api_key="sk-sandbox-test"
)
async with await client.create_session(image="python") as session:
# Execute command
result = await session.exec("python3 -c 'print(2 + 2)'")
print(result.output) # 4
# Write and run a file
await session.write("test.py", "print('Hello!')")
result = await session.exec("python3 test.py")
print(result.output) # Hello!
asyncio.run(main())
Important: Filesystem Location
Always work inside WSL’s Linux filesystem (/home/...), NOT the Windows mount (/mnt/c/...):
# ✅ GOOD - Linux filesystem (ext4, supports overlayfs)
cd ~/sandkasten
sudo ./bin/sandkasten
# ❌ BAD - Windows mount (NTFS, no overlayfs support)
cd /mnt/c/Users/You/sandkasten
sudo ./bin/sandkasten # Will fail!
If you need to edit code from Windows, use an IDE that supports WSL2:
- VS Code: Install “WSL” extension, then
code .from WSL - JetBrains IDEs: Use remote development via SSH to localhost
Troubleshooting
“cgroup v2 not mounted” / cgroup errors
Sandkasten needs unified cgroups (cgroups v2) so it can isolate and limit sandbox processes. By default, WSL2 may boot without them, so the daemon reports a cgroup error and refuses to start.
Option A: Enable unified cgroups in WSL2 (recommended)
You don’t replace your existing .wslconfig — you add kernel boot flags.
-
Edit
C:\Users\<YourUsername>\.wslconfig(in your Windows user profile, not inside WSL).If the file already has
[wsl2]and e.g.nestedVirtualization=true, add thekernelCommandLineline. Full example:[wsl2] nestedVirtualization=true kernelCommandLine=systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=allImportant:
- No spaces before
[wsl2] kernelCommandLineis one line, no quotes- Save the file
- No spaces before
-
Fully reboot WSL (this step is often done wrong):
-
Close all WSL terminals.
-
In PowerShell (on Windows):
wsl --shutdown -
Wait 5–10 seconds so the WSL VM actually stops.
-
Start WSL again:
wsl
-
-
Verify inside WSL:
stat -fc %T /sys/fs/cgroupYou want the output:
cgroup2fsIf you see
cgroup2fs, the runtime can start. Try:sudo ./bin/sandkasten --config sandkasten.yamlThe cgroup error should be gone.
Why this happens: The program checks whether it’s on a Linux system where it can safely isolate processes. Without unified cgroups, WSL behaves like an older compatibility environment. After this change, WSL uses a proper systemd-managed Linux VM with cgroups v2, which sandkasten (and other container runtimes) require.
Option B: Ensure WSL2 (not WSL1)
If you still have issues, confirm you’re on WSL2:
# In PowerShell
wsl --list --verbose
# Should show:
# NAME VERSION
# Ubuntu-22.04 2
If VERSION is 1, upgrade:
wsl --set-version Ubuntu-22.04 2
“overlayfs: upper fs does not support xattrs”
You’re trying to run from /mnt/c/... (NTFS). Move to Linux filesystem:
# Move project to home directory
cp -r /mnt/c/Users/You/sandkasten ~/sandkasten
cd ~/sandkasten
Also ensure data_dir is on Linux filesystem:
# ✅ Correct
data_dir: "/var/lib/sandkasten"
# ❌ Wrong
data_dir: "/mnt/c/sandkasten"
“permission denied” / namespace errors
The daemon needs root for namespace operations:
sudo ./bin/sandkasten --config sandkasten.yaml
“image not found”
Import at least one image:
./bin/imgbuilder list
# If empty:
sudo ./bin/imgbuilder import --name python --tar /tmp/python.tar.gz
Connection refused from Windows
- Check daemon is running in WSL2
- Check WSL2 port forwarding:
# In PowerShell
netstat -an | findstr 8080
- If still not working, find WSL2 IP and use directly:
# In WSL2
ip addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1
# e.g., 172.20.10.2
Then access from Windows: http://172.20.10.2:8080
Go version too old
If go version shows < 1.24:
# Remove old version
sudo apt remove golang-go
# Install manually
wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
# Add to PATH
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
# Verify
go version
Quick Reference
# Build
task build
# Import image
docker create --name temp python:3.12-slim
docker export temp | gzip > /tmp/python.tar.gz && docker rm temp
sudo ./bin/imgbuilder import --name python --tar /tmp/python.tar.gz
# Start daemon
sudo ./bin/sandkasten --config sandkasten.yaml
# Test
curl http://localhost:8080/healthz
API Quick Reference
API_KEY="sk-sandbox-test"
BASE="http://localhost:8080"
# Create session
SESSION=$(curl -s -X POST $BASE/v1/sessions \
-H "Authorization: Bearer $API_KEY" \
-d '{"image":"python"}' | jq -r .id)
# Execute command
curl -s -X POST $BASE/v1/sessions/$SESSION/exec \
-H "Authorization: Bearer $API_KEY" \
-d '{"cmd":"echo hello"}' | jq .
# Write file
curl -s -X POST $BASE/v1/sessions/$SESSION/fs/write \
-H "Authorization: Bearer $API_KEY" \
-d '{"path":"/workspace/test.py","text":"print(42)"}' | jq .
# Read file
curl -s "$BASE/v1/sessions/$SESSION/fs/read?path=/workspace/test.py" \
-H "Authorization: Bearer $API_KEY" | jq -r .content_base64 | base64 -d
# Destroy session
curl -s -X DELETE $BASE/v1/sessions/$SESSION \
-H "Authorization: Bearer $API_KEY"