RetroStack: a modular Docker platform providing scalable, multi‑emulator support for retro gaming. Run emulators standalone or as composable services. Features include multi-arch images (amd64/arm64), profile-based emulator selection, persistent config/saves, gamepad auto-detection, and optional integration with EmulationStation-DE via FIFO control pipes.
Sponsored and maintained by Blackout Secure.
Tip
RetroStack can run standalone — no frontend required. For an optional frontend, see [docker-emulationstation-de](https://github.com/blackoutsecure/docker-emulationstation-de) (also by Blackout Secure).
This project packages upstream emulators (RetroArch, PPSSPP, Dolphin) into ready-to-run container images for cabinets, desktops, HTPCs, and handheld Linux systems. Each image starts its own internal Xorg server and launches the emulator GUI by default (standalone mode) or listens for launch commands via FIFO control pipes when set to daemon mode — ideal for integration with frontends like EmulationStation-DE. No host X server is required.
Quick links:
Note
**Not sure which emulator to pick?** Use RetroArch — it covers the widest range of systems (NES, SNES, GB/GBA, Genesis, PS1, and hundreds more) via libretro cores. It's the default and recommended choice for most users.
Standalone — run a game directly (container exits when done):
docker run --rm \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v /path/to/roms:/roms:ro \
--device=/dev/dri:/dev/dri \
--device=/dev/input:/dev/input \
--device=/dev/snd:/dev/snd \
blackoutsecure/retrostack:retroarch \
--core gambatte /roms/gb/game.gb
Try instantly — no ROMs needed (uses the bundled demo ROM):
docker compose --profile retroarch up -d
The image ships with a free, open-source demo ROM (Libbet and the Magic Floor) that is automatically seeded into /roms on first boot when the volume is empty and writable. If you supply your own ROMs, seeding is skipped entirely and /roms can be mounted read-only.
Service mode — start emulator containers using profiles:
# Start RetroArch emulator container
docker compose --profile retroarch up -d
# Start all emulator containers
docker compose --profile all up -d
For compose examples, device passthrough, Balena deployment, and local build options, see Usage below.
Docker Hub (Recommended):
docker pull blackoutsecure/retrostack:retroarch# Pull RetroArch (default)
docker pull blackoutsecure/retrostack:latest
docker pull blackoutsecure/retrostack:retroarch
# Pull PPSSPP
docker pull blackoutsecure/retrostack:ppsspp
# Pull Dolphin
docker pull blackoutsecure/retrostack:dolphin-emu
RetroStack packages three upstream emulator projects into containerised runtimes. Each runs as an independent service — pick only the emulators you need. RetroArch is the recommended default — it handles the widest range of systems via libretro cores. Use PPSSPP or Dolphin only if you need dedicated PSP or GameCube/Wii support beyond what RetroArch provides.
| Tag | Emulator | Install Method | Upstream | License |
|---|---|---|---|---|
latest | RetroArch + cores | PPA (ppa:libretro/stable) | libretro/RetroArch | GPL-3.0 |
retroarch | RetroArch + cores | PPA (ppa:libretro/stable) | libretro/RetroArch | GPL-3.0 |
ppsspp | PPSSPP (PSP) | Source build | hrydgard/ppsspp | GPL-2.0 |
dolphin-emu | Dolphin (GC/Wii) | Source build | dolphin-emu/dolphin | GPL-2.0 |
All images use ghcr.io/linuxserver/baseimage-ubuntu:noble as the runtime base
(configurable via BASE_IMAGE* build args). Versions are tracked automatically
by upstream monitor workflows and injected at build time via --build-arg.
Upstream project details:
This image is published as a multi-arch manifest. Pulling blackoutsecure/retrostack:latest retrieves the correct image for your host architecture.
The architectures supported by this image are:
| Architecture | Available Tags |
|---|---|
| x86-64 | latest, retroarch, ppsspp, dolphin-emu |
| arm64 | latest, retroarch, ppsspp, dolphin-emu |
Tag scheme:
| Variant | Rolling | Platform-Pinned | Emulator-Pinned | Commit-Pinned |
|---|---|---|---|---|
| RetroArch | latest, retroarch | 1.0.0, 1.0.0-retroarch | retroarch-v1.22.2 | retroarch-sha-<commit> |
| PPSSPP | ppsspp | 1.0.0-ppsspp | ppsspp-v1.20.3 | ppsspp-sha-<commit> |
| Dolphin | dolphin-emu | 1.0.0-dolphin-emu | dolphin-emu-2509 | dolphin-emu-sha-<commit> |
Run a single emulator:
Standalone mode (default — emulator launches its own GUI, seeds bundled demo ROM on first boot):
---
services:
retroarch:
image: blackoutsecure/retrostack:retroarch
container_name: retrostack-retroarch
environment:
DISPLAY: ':0'
PULSE_SERVER: 'unix:/run/pulse/native'
volumes:
- retrostack-config:/config
- retrostack-roms:/roms # writable so demo ROM can be seeded
- retrostack-bios:/bios:ro
- pulse-socket:/run/pulse:ro
devices:
- /dev/dri:/dev/dri
- /dev/input:/dev/input
- /dev/snd:/dev/snd
privileged: true
tmpfs:
- /var/tmp
- /run:exec
shm_size: 1gb
restart: unless-stopped
volumes:
retrostack-config:
retrostack-roms:
retrostack-bios:
pulse-socket:
Tip: If you provide your own ROMs, you can mount
/romsread-only (retrostack-roms:/roms:ro). Demo ROM seeding is only attempted when the volume is empty, so:rowith existing content works without errors.
Daemon / integration mode (ES-DE or another frontend controls the emulator — ROMs are read-only):
---
services:
retroarch:
image: blackoutsecure/retrostack:retroarch
container_name: retrostack-retroarch
environment:
DISPLAY: ':0'
PULSE_SERVER: 'unix:/run/pulse/native'
RETROSTACK_FRONTEND_MODE: 'daemon'
volumes:
- retrostack-config:/config
- retrostack-roms:/roms:ro # read-only — frontend owns the ROMs
- retrostack-bios:/bios:ro
- retrostack-emulator-control:/run/retrostack-emulators
- pulse-socket:/run/pulse:ro
devices:
- /dev/dri:/dev/dri
- /dev/input:/dev/input
- /dev/snd:/dev/snd
privileged: true
tmpfs:
- /var/tmp
- /run:exec
shm_size: 1gb
restart: unless-stopped
volumes:
retrostack-config:
retrostack-roms:
retrostack-bios:
retrostack-emulator-control:
pulse-socket:
Using profiles from the included docker-compose.yml:
# Start RetroArch only
docker compose --profile retroarch up -d
# Start all emulators
docker compose --profile all up -d
Standalone game launch (container exits when done):
# Game Boy game with RetroArch + gambatte core
docker run --rm \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v /path/to/roms:/roms:ro \
--device=/dev/dri:/dev/dri \
--device=/dev/input:/dev/input \
--device=/dev/snd:/dev/snd \
blackoutsecure/retrostack:retroarch \
--core gambatte /roms/gb/game.gb
# PSP game with PPSSPP
docker run --rm \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v /path/to/roms:/roms:ro \
--device=/dev/dri:/dev/dri \
blackoutsecure/retrostack:ppsspp \
/roms/psp/game.iso
# GameCube game with Dolphin
docker run --rm \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v /path/to/roms:/roms:ro \
--device=/dev/dri:/dev/dri \
blackoutsecure/retrostack:dolphin-emu \
/roms/gc/game.iso
Daemon mode (container waits for a launch command, then exits after the game ends):
docker run -d \
--name=retrostack-retroarch \
--restart unless-stopped \
-e DISPLAY=:0 \
-e PULSE_SERVER=unix:/run/pulse/native \
-e RETROSTACK_IDLE_TIMEOUT=600 \
-e RETROSTACK_FRONTEND_MODE=daemon \
-v retrostack-emulator-control:/run/retrostack-emulators \
-v retrostack-shared:/run/retrostack-shared:ro \
-v retrostack-config:/config \
-v retrostack-roms:/roms:ro \
-v retrostack-bios:/bios:ro \
-v x11-unix:/tmp/.X11-unix:ro \
-v pulse-socket:/run/pulse:ro \
--device=/dev/dri:/dev/dri \
--device=/dev/input:/dev/input \
--device=/dev/snd:/dev/snd \
--shm-size=1gb \
blackoutsecure/retrostack:retroarch
This image can be deployed to Balena-powered devices using the included docker-compose.yml file (Balena labels are included and harmlessly ignored by standard Docker).
balena push <your-app-slug>
See Balena documentation for details.
When used with docker-emulationstation-de, both containers share a control volume and the same X11 display:
┌──────────────────────────────┐ ┌──────────────────────────┐
│ RetroStack │ │ emulationstation-de │
│ (this repo) │ │ (separate repo) │
│ │ │ │
│ Emulator binary stays here │ control pipe │ User selects game │
│ Listens on FIFO for launch │◀────────────────│ retrostack-emulator- │
│ commands, runs emulator on │ /run/retro*/ │ launch writes to FIFO │
│ shared X11 display │────────────────▶│ reads exit code back │
│ │ exit status │ │
└──────────────────────────────┘ └──────────────────────────┘
│ │
├── /dev/dri (GPU) ├── /dev/dri (GPU)
├── /dev/input (controllers) ├── /dev/input
├── /dev/snd (audio) ├── /dev/snd
└── X11 socket └── X11 socket
Both containers share a volume at /run/retrostack-emulators/. Each emulator creates:
| File | Direction | Purpose |
|---|---|---|
<name>.cmd | ES-DE → Emulator | FIFO — write emulator args (one line, shell-quoted) |
<name>.status | Emulator → ES-DE | FIFO — read exit code after game finishes |
/run/retrostack-emulators/<name>.cmd and .status, then waits for a launch command (or times out after RETROSTACK_IDLE_TIMEOUT seconds)retrostack-emulator-launch and symlinks each emulator name to it (e.g. retroarch → retrostack-emulator-launch)retrostack-emulator-launch writes the args to the .cmd pipe, the emulator container reads it and runs the game on the shared display.status pipe. retrostack-emulator-launch reads it and returns, giving control back to ES-DE. The emulator container then stops.volumes:
emulationstation-config:
emulationstation-roms:
emulationstation-bios:
retrostack-emulator-control:
retrostack-shared:
x11-unix:
pulse-socket:
services:
emulationstation:
image: blackoutsecure/emulationstation-de:latest
container_name: emulationstation
environment:
TZ: 'Etc/UTC'
DISPLAY_NUM: '0'
XDG_RUNTIME_DIR: '/run/esde'
ESDE_USE_INTERNAL_X: '1'
UDEV: '1'
volumes:
- emulationstation-config:/config
- emulationstation-roms:/roms:ro
- emulationstation-bios:/bios:ro
- retrostack-emulator-control:/run/retrostack-emulators
- x11-unix:/tmp/.X11-unix:ro
- pulse-socket:/run/pulse:ro
devices:
- /dev/dri:/dev/dri
- /dev/input:/dev/input
- /dev/snd:/dev/snd
privileged: true
shm_size: 1gb
restart: unless-stopped
retroarch:
image: blackoutsecure/retrostack:retroarch
container_name: retrostack-retroarch
environment:
DISPLAY: ':0'
PULSE_SERVER: 'unix:/run/pulse/native'
RETROSTACK_FRONTEND_MODE: 'daemon'
volumes:
- retrostack-emulator-control:/run/retrostack-emulators
- retrostack-shared:/run/retrostack-shared:ro
- emulationstation-roms:/roms:ro
- emulationstation-bios:/bios:ro
- x11-unix:/tmp/.X11-unix:ro
- pulse-socket:/run/pulse:ro
devices:
- /dev/dri:/dev/dri
- /dev/input:/dev/input
- /dev/snd:/dev/snd
shm_size: 1gb
restart: unless-stopped
Install retrostack-emulator-launch in the ES-DE container and create symlinks for each emulator:
# Copy the launch script from the RetroStack image
docker cp retrostack-retroarch:/usr/local/bin/retrostack-emulator-launch /usr/local/bin/
# Create symlinks — ES-DE calls these by name
ln -sf /usr/local/bin/retrostack-emulator-launch /usr/local/bin/retroarch
ln -sf /usr/local/bin/retrostack-emulator-launch /usr/local/bin/PPSSPPSDL
ln -sf /usr/local/bin/retrostack-emulator-launch /usr/local/bin/dolphin-emu
[retrostack:retroarch] Daemon mode — version: RetroArch 1.22.2 (Git ...)
[retrostack:retroarch] Available cores: 6
[retrostack:retroarch] Control pipe: /run/retrostack-emulators/retroarch.cmd
[retrostack:retroarch] Idle timeout: 600s
[retrostack:retroarch] Ready — waiting for launch commands from frontend (e.g. ES-DE)
After a game finishes:
[retrostack:retroarch] Launch: --core gambatte /roms/gb/game.gb
[retrostack:retroarch] Exited: 0
[retrostack:retroarch] Emulator process ended — stopping container.
If no launch command is received within the idle timeout:
[retrostack:retroarch] Idle timeout (600s) reached — no launch commands received. Exiting.
| Parameter | Description | Required |
|---|---|---|
-e EMULATOR_NAME | Emulator identifier (set in image — retroarch, ppsspp, dolphin-emu) | Set per target |
-e EMULATOR_BINARY | Path to emulator binary | Set per target |
-e EMULATOR_CORE | Default libretro core for RetroArch | Optional |
-e DISPLAY=:0 | X11 display | Optional |
-e PULSE_SERVER | PulseAudio server path | Optional |
-e XDG_RUNTIME_DIR | Runtime directory for display/session (default: /run/retrostack) | Optional |
-e RETROSTACK_EMULATORS_CONTROL | Control pipe directory (client-side) | Optional |
-e RETROSTACK_IDLE_TIMEOUT | Seconds to wait for a launch command before the container exits (default: 600, set to 0 to disable) | Optional |
-e RETROSTACK_FRONTEND_MODE | standalone (default) launches the emulator's own GUI; daemon listens on FIFO for ES-DE integration | Optional |
-e RETROSTACK_USE_INTERNAL_X | Start an internal Xorg server in standalone mode (default: 1). Set to 0 to use an external X socket | Optional |
| Mount | Description | Required |
|---|---|---|
retrostack-config:/config | Persistent emulator settings, saves, and states | Recommended |
retrostack-roms:/roms | ROM library — writable by default for demo ROM seeding on first boot; can use :ro if you supply your own ROMs or in daemon/integration mode | Recommended |
retrostack-bios:/bios:ro | BIOS files for emulators that need them | Optional |
pulse-socket:/run/pulse:ro | PulseAudio socket | Optional |
/tmp/.X11-unix:/tmp/.X11-unix:ro | X11 socket (only when RETROSTACK_USE_INTERNAL_X=0) | Conditional |
retrostack-emulator-control:/run/retrostack-emulators | FIFO control pipe volume (daemon mode / ES-DE only) | Daemon only |
retrostack-shared:/run/retrostack-shared:ro | Shared runtime — gamepad mappings, Xauthority (ES-DE only) | Daemon only |
| Device | Description | R
Content type
Image
Digest
sha256:aa46ab9f6…
Size
434.3 MB
Last updated
6 months ago
docker pull blackoutsecure/retrostack