Skip to content

Phase 0: A GPU, a container, and a window on Windows

Goal: one command builds a robotics environment, and it renders on the GPU, inside Docker, inside WSL, on a Windows laptop.

Picking a stack (in 2026)

Chris last used ROS 2 a while ago. It's still the standard, so we chose:

Layer Choice Why
Middleware ROS 2 Jazzy (LTS to 2029) Same Ubuntu (24.04) as the WSL host
Simulator Gazebo Harmonic Jazzy's paired release, good for outdoor ground robots
Observability Elasticsearch + Kibana 9.5.3 Chris loves Kibana dashboards
Everything else Docker Compose + Makefile Repeatable, pinned, scriptable by an agent

We passed on Isaac Sim for now. It's gorgeous, but heavy, it isn't officially supported on WSL, and 12 GB of VRAM is tight.

The interesting trick: OpenGL through WSL

CUDA in Docker on WSL is easy (gpus: all). OpenGL rendering is the subtle part: Gazebo needs a real GPU, or it silently falls back to slow CPU rendering (llvmpipe). The recipe:

devices: [/dev/dxg]                        # WSL's GPU paravirtualization device
volumes: [/usr/lib/wsl:/usr/lib/wsl:ro]    # Windows driver userspace libs
environment:
  GALLIUM_DRIVER: d3d12                     # Mesa -> Direct3D 12 -> NVIDIA
  MESA_D3D12_DEFAULT_ADAPTER_NAME: NVIDIA

And make gpu-check proves it:

NVIDIA GeForce RTX 4080 Laptop GPU
OpenGL renderer string: D3D12 (NVIDIA GeForce RTX 4080 Laptop GPU)
OpenGL core profile version string: 4.6 (Core Profile) Mesa 25.2.8

What broke

  • gz: command not found. Non-interactive shells skip .bashrc, so ROS was never sourced. Fixed with a tiny scripts/in_ros.sh wrapper that every make target goes through.
  • Kibana "unavailable". It's slow to start (about a minute of migrations), and with no healthcheck make up returned too early. We added a real healthcheck.
  • Kibana encryption keys were randomly generated on every start, which breaks saved objects across restarts. We pinned local-only keys.
  • Claude wrote a broken Makefile line (a bash -c quoting trick that would have launched an empty shell), caught it on review, and replaced it with the wrapper script. Pair programming goes both ways.

Result

make doctor     ✔ 6/6 host checks
make gpu-check  ✔ CUDA + OpenGL 4.6 on the 4080
make sim-smoke  ✔ 2000 physics steps, headless
make es-check   ✔ ES green, Kibana available

Lessons learned

  • Pin the base image by digest, not by tag. Tags move; digests don't.
  • A health check that isn't checked (--wait with no healthcheck) is just optimism.