Sign inSign up

blackoutsecure/retrostack

By blackoutsecure

•Updated 5 months ago

Image
0

10K+

blackoutsecure/retrostack repository overview

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](https://github.com/blackoutsecure/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.

TagEmulatorInstall MethodUpstreamLicense
latestRetroArch + coresPPA (ppa:libretro/stable)libretro/RetroArch⁠GPL-3.0
retroarchRetroArch + coresPPA (ppa:libretro/stable)libretro/RetroArch⁠GPL-3.0
ppssppPPSSPP (PSP)Source buildhrydgard/ppsspp⁠GPL-2.0
dolphin-emuDolphin (GC/Wii)Source builddolphin-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:

ArchitectureAvailable Tags
x86-64latest, retroarch, ppsspp, dolphin-emu
arm64latest, retroarch, ppsspp, dolphin-emu

Tag scheme:

VariantRollingPlatform-PinnedEmulator-PinnedCommit-Pinned
RetroArchlatest, retroarch1.0.0, 1.0.0-retroarchretroarch-v1.22.2retroarch-sha-<commit>
PPSSPPppsspp1.0.0-ppssppppsspp-v1.20.3ppsspp-sha-<commit>
Dolphindolphin-emu1.0.0-dolphin-emudolphin-emu-2509dolphin-emu-sha-<commit>

⁠Usage

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
⁠Docker CLI (click here for more info⁠)

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:

FileDirectionPurpose
<name>.cmdES-DE → EmulatorFIFO — write emulator args (one line, shell-quoted)
<name>.statusEmulator → ES-DEFIFO — 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
ParameterDescriptionRequired
-e EMULATOR_NAMEEmulator identifier (set in image — retroarch, ppsspp, dolphin-emu)Set per target
-e EMULATOR_BINARYPath to emulator binarySet per target
-e EMULATOR_COREDefault libretro core for RetroArchOptional
-e DISPLAY=:0X11 displayOptional
-e PULSE_SERVERPulseAudio server pathOptional
-e XDG_RUNTIME_DIRRuntime directory for display/session (default: /run/retrostack)Optional
-e RETROSTACK_EMULATORS_CONTROLControl pipe directory (client-side)Optional
-e RETROSTACK_IDLE_TIMEOUTSeconds to wait for a launch command before the container exits (default: 600, set to 0 to disable)Optional
-e RETROSTACK_FRONTEND_MODEstandalone (default) launches the emulator's own GUI; daemon listens on FIFO for ES-DE integrationOptional
-e RETROSTACK_USE_INTERNAL_XStart an internal Xorg server in standalone mode (default: 1). Set to 0 to use an external X socketOptional
⁠Storage Mounts
MountDescriptionRequired
retrostack-config:/configPersistent emulator settings, saves, and statesRecommended
retrostack-roms:/romsROM library — writable by default for demo ROM seeding on first boot; can use :ro if you supply your own ROMs or in daemon/integration modeRecommended
retrostack-bios:/bios:roBIOS files for emulators that need themOptional
pulse-socket:/run/pulse:roPulseAudio socketOptional
/tmp/.X11-unix:/tmp/.X11-unix:roX11 socket (only when RETROSTACK_USE_INTERNAL_X=0)Conditional
retrostack-emulator-control:/run/retrostack-emulatorsFIFO control pipe volume (daemon mode / ES-DE only)Daemon only
retrostack-shared:/run/retrostack-shared:roShared runtime — gamepad mappings, Xauthority (ES-DE only)Daemon only
⁠Devices

| Device | Description | R

Tag summary

Content type

Image

Digest

sha256:aa46ab9f6…

Size

434.3 MB

Last updated

6 months ago

docker pull blackoutsecure/retrostack