Skip to content

blackoutsecure/docker-retrostack

RetroStack logo

RetroStack

GitHub Stars Docker Pulls GitHub Release Docker CI License

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 (also by Blackout Secure).

Overview

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:


Table of Contents


Quick Start

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.


Image Availability

Docker Hub (Recommended):

  • All images are published to Docker Hub
  • Simple pull command: docker pull blackoutsecure/retrostack:retroarch
  • Multi-arch support: amd64, arm64
  • No registry prefix needed when pulling from Docker Hub
# 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

About The Emulators

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:


Supported Architectures

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>

Usage

Docker Compose (recommended, click here for more info)

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 /roms read-only (retrostack-roms:/roms:ro). Demo ROM seeding is only attempted when the volume is empty, so :ro with 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

Balena Deployment

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.


ES-DE Integration

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

Control Pipe Protocol

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

How It Works

  1. Startup: Emulator container creates FIFO pipes at /run/retrostack-emulators/<name>.cmd and .status, then waits for a launch command (or times out after RETROSTACK_IDLE_TIMEOUT seconds)
  2. Discovery: ES-DE installs retrostack-emulator-launch and symlinks each emulator name to it (e.g. retroarch → retrostack-emulator-launch)
  3. Game launch: When the user selects a game, ES-DE calls the symlink. retrostack-emulator-launch writes the args to the .cmd pipe, the emulator container reads it and runs the game on the shared display
  4. Return: When the game exits, the emulator container writes the exit code to the .status pipe. retrostack-emulator-launch reads it and returns, giving control back to ES-DE. The emulator container then stops.

Combined docker-compose.yml

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

ES-DE Side Setup

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

Startup Log Output

[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.

Parameters

Environment Variables

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

Storage Mounts

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

Devices

Device Description Required
--device=/dev/dri:/dev/dri GPU passthrough for Intel/AMD rendering Optional
--device=/dev/input:/dev/input Gamepad and input passthrough Optional
--device=/dev/snd:/dev/snd Audio device passthrough Optional

Runtime Security Defaults

Setting Value Purpose
read_only false Keep root filesystem writable for LSIO init ownership setup
tmpfs /var/tmp /run writable Writable runtime scratch paths
shm_size 1gb Shared memory for SDL and rendering stability

Configuration

The container stores persistent emulator data under /config/<emulator-name>/.

/config - Emulator Settings and Persistence

  • Required: No, but recommended if you want settings and saves to survive restarts
  • Purpose: Stores emulator configuration, save games, save states, and logs
  • Example: Named volume retrostack-config:/config

/roms - Content Library

  • Required: Recommended
  • Purpose: ROM library for the emulator to browse and play
  • Default (writable): On first boot, if the volume is empty, the bundled demo ROM (Libbet and the Magic Floor) is automatically seeded so you can start playing immediately
  • Read-only (:ro): Safe to use when you supply your own ROMs — seeding is skipped when content already exists. Also recommended for daemon/integration mode where the frontend (e.g. ES-DE) owns the ROM library

/bios - Emulator Support Files

  • Required: Optional
  • Purpose: Supply BIOS files used by emulator backends that need them
  • Example: Named volume retrostack-bios:/bios:ro

Best Practices

  • Keep /config persistent so emulator saves and settings survive container recreation
  • Leave /roms writable for the default out-of-box experience — the bundled demo ROM is seeded on first boot when the volume is empty
  • Mount /roms read-only (:ro) if you supply your own ROMs or in daemon/integration mode — seeding is safely skipped when content already exists
  • Mount /bios read-only unless you have a specific reason to allow writes
  • Use the same ROM and BIOS volume mounts as your ES-DE container when using ES-DE integration

Adding a New Emulator

  1. Add a FROM ... AS <name> stage to the Dockerfile (with a builder stage if compiling from source).
  2. Set ENV EMULATOR_NAME=<name>, ENV EMULATOR_BINARY=/path/to/binary, and ENV DISPLAY=:0.
  3. Add a service in docker-compose.yml with the control volume and GPU/input/display mounts.
  4. Add a matrix entry in publish.yml (docker + manifest jobs) and upstream-monitor.yml.
  5. On the ES-DE side, symlink retrostack-emulator-launch as the emulator name.

Project layout:

Path Purpose
root/usr/local/lib/retrostack-lib.sh Shared functions and constants (sourced by all scripts)
root/usr/local/bin/retrostack-emulator-run Container entrypoint (daemon + standalone modes)
root/usr/local/bin/retrostack-emulator-launch Client-side FIFO launcher (installed in ES-DE container)
root/usr/local/bin/retrostack-provision Export emulator binary + libs to shared volume
root/etc/s6-overlay/s6-rc.d/ s6-overlay service definitions
VERSION RetroStack platform version (semver)
.github/upstream/*.json Tracked upstream emulator versions

Build Locally

# Build all emulators
docker compose --profile all build

# Build a single emulator
docker build --target retroarch -t blackoutsecure/retrostack:retroarch .
docker build --target ppsspp -t blackoutsecure/retrostack:ppsspp .
docker build --target dolphin-emu -t blackoutsecure/retrostack:dolphin-emu .

# Override a tracked version
docker build --build-arg PPSSPP_VERSION=v1.20.3 --target ppsspp .
docker build --build-arg RETROARCH_VERSION=1.22.2 --target retroarch .

Troubleshooting

Emulator not launching

  • Verify the emulator container is running: docker ps | grep retrostack-
  • Check container logs: docker logs retrostack-retroarch
  • Ensure /dev/dri is passed through for GPU access
  • Verify /tmp/.X11-unix is mounted for X11 display

Control pipe errors

  • Ensure the retrostack-emulator-control volume is shared between ES-DE and the emulator container
  • Check that the emulator container started successfully and created the FIFO pipes
  • Look for [retrostack] ERROR: pipe not found in ES-DE logs
  • Start the emulator container: docker compose --profile retroarch up -d

Audio issues

  • Ensure /dev/snd is passed through or PULSE_SERVER is set
  • PulseAudio socket must be accessible at /run/pulse/native

Input devices not detected

  • Ensure the container has access to /dev/input
  • Use privileged: true for full device access in kiosk/cabinet setups

Gamepad Mapping

Each emulator image bundles the SDL_GameControllerDB community database (~3000 known gamepads). When running with ES-DE, the shared gamepad DB from the ES-DE container takes priority.

Mapping priority (highest first):

Priority Source Description
1 SDL_GAMECONTROLLERCONFIG env var User manual overrides
2 Shared DB from ES-DE (/run/retrostack-shared/gamecontrollerdb.txt) ES-DE sidecar mappings
3 Bundled community gamecontrollerdb.txt ~3000 known gamepads
4 SDL2 built-in DB Major brand controllers (Xbox, PlayStation, Switch)

If your gamepad isn't recognized:

  1. Find your gamepad's SDL2 GUID — run sdl2-jstest --list inside the container
  2. Generate a correct mapping at SDL_GameControllerDB or General Arcade Gamepad Tool
  3. Set the mapping in your compose environment:
environment:
  SDL_GAMECONTROLLERCONFIG: "03000000790000001100000000000000,DragonRise Generic USB Joystick,a:b2,b:b1,..."

Upstream Monitoring

A GitHub Actions workflow monitors all three emulator upstreams every 6 hours:

Emulator Monitors Rebuilds
RetroArch libretro/RetroArch releases + PPA version retroarch target
PPSSPP hrydgard/ppsspp releases ppsspp target
Dolphin dolphin-emu/dolphin releases/tags dolphin-emu target

Tracked versions are stored in .github/upstream/*.json and read by the publish/release workflows.


Release & Versioning

RetroStack uses a dual-version scheme: a platform version for the packaging/scripts and independent emulator versions tracked from upstream.

Platform Version

The RetroStack platform version (packaging, scripts, s6 services, CI) is tracked in the VERSION file at the repo root and follows Semantic Versioning:

  • Major: Breaking changes to the control pipe protocol, volume layout, or environment interface
  • Minor: New emulator targets, new features, non-breaking config changes
  • Patch: Bug fixes, dependency updates, documentation

Current version: 1.0.0

Emulator Versions

Each emulator tracks its own upstream release independently. Versions are stored in .github/upstream/*.json and resolved at build time:

Emulator Version Source Tracked File
RetroArch libretro/RetroArch releases + PPA .github/upstream/retroarch-release.json
PPSSPP hrydgard/ppsspp releases .github/upstream/ppsspp-release.json
Dolphin dolphin-emu/dolphin releases .github/upstream/dolphin-release.json

Tag Scheme

Tag Pattern Example Description
:latest :latest Rolling latest (RetroArch)
:<target> :retroarch Rolling latest for emulator
:<target>-<emu-version> :retroarch-v1.22.2 Pinned to emulator version
:<rs-version>-<target> :1.0.0-retroarch Pinned to RetroStack platform version
:<rs-version> :1.0.0 Platform-pinned (RetroArch default)
:<target>-sha-<commit> :retroarch-sha-abc123 Commit-pinned

Image Labels

Each image includes OCI and RetroStack-specific labels:

Label Value
org.opencontainers.image.version RetroStack platform version
org.opencontainers.image.vendor Blackout Secure
io.retrostack.version RetroStack platform version
io.retrostack.emulator Emulator name (retroarch, ppsspp, dolphin-emu)
io.retrostack.emulator.version Upstream emulator version

CI Workflows


Support & Getting Help


References

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

  •  

Packages

 
 
 

Contributors