I run my gaming setup in Docker.
More specifically, I use docker-steam-headless, which gives me a full Steam desktop, Sunshine, X11, GPU acceleration, and remote access through Moonlight.
It works surprisingly well. I can stream my desktop or Steam Big Picture from a server to almost any device.
But there was one annoying problem: resolution switching.
I do not always connect from the same screen. Sometimes I use an ultrawide monitor. Sometimes a laptop. Sometimes a 16:10 display. Sometimes Moonlight is configured for 2560x1080, sometimes 2560x1600, sometimes 1920x1080.
I packaged the fix into a small reusable overlay: steam-headless-adaptive-resolution
The problem
Sunshine streams whatever the X server gives it.
In a normal desktop setup, this is usually fine. You have a physical display and the desktop environment can react to resolution changes.
In a headless container, however, the display is virtual. With docker-steam-headless, X11 exposes a virtual output, and Sunshine captures it.
When switching between clients with different aspect ratios, I would sometimes end up with broken states like this:
Screen 0: current 2560 x 1600
HDMI-0 connected primary 2560x1080+0+0
It means the X11 framebuffer is 2560x1600, but the actual output is still 2560x1080.
At first, I thought Sunshine was not receiving the correct resolution. But the logs showed otherwise.
Moonlight was sending the right resolution. Sunshine was seeing it. X11 just was not always being updated correctly.
The important detail
The fix was not just “change resolution with xrandr”.
I had already tried that.
The important part was the order:
1. Change the XRandR output mode
2. Then update the X11 framebuffer
Not the other way around.
The working sequence looks like this:
xrandr \
--output "$OUTPUT" \
--primary \
--mode "$MODE" \
--pos 0x0 \
--rotate normal
xrandr --fb "${WIDTH}x${HEIGHT}"
After that, XRandR should show matching values:
Screen 0: current 2560 x 1600
HDMI-0 connected primary 2560x1600+0+0
Once both lines match, Sunshine captures the correct virtual desktop and Moonlight displays it properly.
The clean overlay
I wanted the solution to be reusable and not require rebuilding the Docker image.
The repo is just an overlay:
.
├── docker-compose.override.yml
├── install.sh
├── uninstall.sh
├── install/
│ └── 99-install-res-switch.sh
└── scripts/
├── res-switch
└── res-reset
The Compose override mounts the scripts:
services:
steam-headless:
volumes:
- ./scripts/res-switch:/home/default/bin/res-switch:ro
- ./scripts/res-reset:/home/default/bin/res-reset:ro
- ./install/99-install-res-switch.sh:/opt/res-switch/99-install-res-switch.sh:ro
Patching Sunshine
The installer adds this prep-cmd to the existing Desktop app:
{
"do": "bash /home/default/bin/res-switch",
"undo": "bash /home/default/bin/res-reset",
"elevated": false
}
So when Moonlight starts a Desktop session, Sunshine runs res-switch, and the display is resized before streaming begins.
The installer is copied into:
/home/default/init.d/99-install-res-switch.sh
so the patch survives container restarts and can be reapplied if apps.json changes.
Installation
From the same directory as the docker-compose.yml:
./install.sh
The installer:
- starts the container;
- waits for Supervisor to become ready;
- copies the installer into the persistent home volume;
- patches Sunshine’s generated
apps.json; - restarts Sunshine.
After installation, you can verify the patch with:
docker compose exec steam-headless grep -A10 -B3 res-switch /home/default/.config/sunshine/apps.json
Expected output:
"prep-cmd": [
{
"do": "bash /home/default/bin/res-switch",
"undo": "bash /home/default/bin/res-reset",
"elevated": false
}
]
Testing
After connecting with Moonlight, you can watch the script log:
docker compose exec steam-headless tail -f /home/default/.cache/log/res-switch.log
A successful switch looks like this:
Requested by Moonlight: 2560x1600@60
Using output: HDMI-0
Using existing mode: 2560x1600
Applying output mode: 2560x1600
Applying framebuffer: 2560x1600
Done.
And XRandR should confirm it:
docker compose exec steam-headless xrandr -q | head -n 20
Screen 0: current 2560 x 1600
HDMI-0 connected primary 2560x1600+0+0
Refresh rate weirdness
One thing I learned along the way: custom refresh rates can be fragile in this setup.
For example, 2560x1600@75 caused problems on my virtual NVIDIA output, while 2560x1600@60 worked immediately because the mode already existed.
So the script prefers existing XRandR modes when possible, and only creates custom modes as a fallback.
If something breaks, the first thing to try is 60 FPS.
Conclusion
This was one of those fixes that sounds simple but takes a while to get right.
Once all of those agree, the setup works beautifully.
Now we can switch between ultrawide, 16:10, and regular 16:9 Moonlight clients, and the headless Steam container follows automatically.
If you use docker-steam-headless with Moonlight, the project might save you a few hours of debugging:
steam-headless-adaptive-resolution
And if it does, stars are always appreciated :)