-
Notifications
You must be signed in to change notification settings - Fork 0
Installation
GamesDownloader ships as a set of Docker containers orchestrated by Docker Compose: the application itself, a MariaDB database, and a Redis-compatible cache/queue (Valkey). This page takes you from nothing to a running instance, explains every setting in the .env file, and covers first boot, updating, and resetting.
Just want the short version? Copy
.env.exampleto.env, change the four passwords, rundocker compose up -d, and openhttp://localhost:8080. Everything below is the detail behind those four steps.
- Requirements
- Step 1: Get the files
- Step 2: Configure
.env - Step 3: Start the stack
- First boot: what happens
- The setup wizard
- Post-install steps
- Where your data lives
- Running as a non-root user (PUID/PGID)
- Updating
- Verifying the install
- Resetting / uninstalling
| Requirement | Notes |
|---|---|
| Docker | Engine 24+ recommended. Install guide. |
| Docker Compose | v2 (the docker compose subcommand, not the legacy docker-compose). |
| Disk | The app image plus MariaDB and Valkey are modest, but your library, ROMs, downloads, and artwork can grow to hundreds of GB. Point GD_BASE_DIR at a roomy disk. First boot also pulls a ~150 MB LaunchBox metadata file and (optionally) ~300 MB of ClamAV virus definitions. |
| RAM | 2 GB is enough for a small library; 4 GB+ is comfortable once scraping and emulation are in use. |
| Architecture | Prebuilt images are published for linux/amd64 and linux/arm64 (Raspberry Pi 4/5, Apple Silicon under Linux VMs, etc.). |
| Ports |
8080 (web UI) is required. 9091 (Transmission WebUI) and 51413/tcp+udp (torrent peers) are only needed if you use torrents. |
GamesDownloader is designed to run happily LAN-only with no reverse proxy. HTTPS, a domain, and a proxy are optional and only relevant if you expose it beyond your local network - see Reverse Proxy & HTTPS.
You need two files: docker-compose.yml and your .env, side by side in a folder of their own.
The application container is fully self-contained. A single image bundles everything the app needs: the FastAPI backend, the compiled Vue frontend, a self-hosted copy of EmulatorJS (so in-browser emulation works with no external CDN), the ClamAV antivirus daemon, the Transmission torrent daemon, ffmpeg, and a Node.js + Vite toolchain used to compile theme-plugin .vue files at startup. You do not install any of those separately. Only MariaDB and Valkey run as their own containers.
There are two ways to get going. Most people should use Option A, which needs no clone and no build: you just create the two files below and start. Use Option B only if you want to build the image yourself from source.
No local build, no Node or Python toolchain on your host, no wait. Multi-arch images (linux/amd64 and linux/arm64) are published to Docker Hub automatically for every release. Docker pulls the right architecture for your machine from the same tag.
1. Create docker-compose.yml. Make a folder for your instance and save this complete file into it as docker-compose.yml. It is ready to run as-is; every value is driven by your .env (next step):
# GamesDownloader - Docker Compose (prebuilt image from Docker Hub)
# All persistent data is stored under GD_BASE_DIR (set in .env).
services:
# ── Main application ─────────────────────────────────────────────────────
gamesdownloader:
image: 60plus/gamesdownloader:latest
container_name: GamesDownloader
restart: unless-stopped
environment:
- PUID=${PUID:-}
- PGID=${PGID:-}
- DB_HOST=gd-mariadb
- DB_PORT=3306
- DB_NAME=${DB_NAME:-gamesdownloader}
- DB_USER=${DB_USER:-gd}
- DB_PASSWD=${DB_PASSWD:-gd}
- REDIS_HOST=gd-redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD:-gd_redis_2026}
- GD_AUTH_SECRET_KEY=${GD_AUTH_SECRET_KEY:-change-me-in-production}
- GD_BASE_PATH=/data
- GD_DEBUG=${GD_DEBUG:-false}
# Scraper credentials (IGDB, RAWG, ScreenScraper, SteamGridDB, RetroAchievements)
# are configured in the app UI (Settings -> Metadata) and stored encrypted at rest,
# NOT via environment variables.
ports:
- "${GD_PORT:-8080}:8080" # web UI (required)
- "${TR_WEBUI_PORT:-9091}:9091" # Transmission WebUI (torrents only)
- "${TR_PEER_PORT:-51413}:51413" # torrent peers (TCP)
- "${TR_PEER_PORT:-51413}:51413/udp" # torrent peers (UDP)
volumes:
- ${GD_BASE_DIR:-.}/data/games:/data/games
- ${GD_BASE_DIR:-.}/data/downloads:/data/downloads
- ${GD_BASE_DIR:-.}/data/resources:/data/resources
- ${GD_BASE_DIR:-.}/data/config:/data/config
- ${GD_BASE_DIR:-.}/data/plugins:/data/plugins
- ${GD_BASE_DIR:-.}/data/clamav:/data/clamav
dns:
- 8.8.8.8
- 1.1.1.1
depends_on:
gd-mariadb:
condition: service_healthy
gd-redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s # safety margin while first-boot init settles
# ── MariaDB ──────────────────────────────────────────────────────────────
gd-mariadb:
image: mariadb:11.3
container_name: gd-mariadb
restart: unless-stopped
environment:
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWD:-rootpassword}
- MARIADB_DATABASE=${DB_NAME:-gamesdownloader}
- MARIADB_USER=${DB_USER:-gd}
- MARIADB_PASSWORD=${DB_PASSWD:-gd}
volumes:
- ${GD_BASE_DIR:-.}/data/db:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mariadb -u${DB_USER:-gd} -p${DB_PASSWD:-gd} ${DB_NAME:-gamesdownloader} -e 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# ── Redis (Valkey) ─────────────────────────────────────────────────────────
gd-redis:
image: valkey/valkey:8
container_name: gd-redis
restart: unless-stopped
command: valkey-server --requirepass ${REDIS_PASSWORD:-gd_redis_2026}
volumes:
- ${GD_BASE_DIR:-.}/data/redis:/data
healthcheck:
test: ["CMD", "valkey-cli", "-a", "${REDIS_PASSWORD:-gd_redis_2026}", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s2. Create .env. In the same folder, create a .env file. Here is a ready-to-edit template - change every password before you start. Each variable is explained in detail in Step 2 below:
# ── Host paths and port ───────────────────────────────────────
# Base directory for ALL persistent data on the host.
# Default "." keeps everything in a ./data folder next to this compose file.
# To use another disk, set an absolute path, e.g.:
# GD_BASE_DIR=/opt/gamesdownloader
# GD_BASE_DIR=/mnt/storage/games
GD_BASE_DIR=.
GD_PORT=8080
# Optional: run as a specific host user (leave empty to run as root)
PUID=
PGID=
GD_DEBUG=false
# ── Database (CHANGE THESE) ───────────────────────────────────
DB_NAME=gamesdownloader
DB_USER=gd
DB_PASSWD=change-me-strong-random
DB_ROOT_PASSWD=change-me-root-strong-random
# ── Redis / Valkey (CHANGE THIS) ──────────────────────────────
REDIS_PASSWORD=change-me-redis-strong-random
# ── Auth secret (leave default to auto-generate on first start) ─
GD_AUTH_SECRET_KEY=change-me-in-production
# ── Metadata scrapers: NOT set here. Configure them in the app UI (Setup Wizard
# or Settings -> Metadata), where they are stored encrypted at rest.3. Pull the image and start:
docker compose pull
docker compose up -dThen jump to First boot: what happens.
Prefer to pin a version? The compose above uses latest, the simplest choice: every docker compose pull moves you to the newest release. If you would rather control exactly when you upgrade, replace latest in the image: line with one of these tags:
| Tag | Example | Behaviour | Use when |
|---|---|---|---|
latest |
60plus/gamesdownloader:latest |
Always the newest release. Moves forward every time you pull. |
Default in the compose above. You want the newest version and upgrade on each pull. |
MAJOR.MINOR |
60plus/gamesdownloader:1.0 |
Rolling within a minor line; picks up patch releases (1.0.16, 1.0.17, ...) but not a future 1.1. |
You want automatic patch/bugfix updates but a stable feature set. |
MAJOR.MINOR.PATCH |
60plus/gamesdownloader:1.0.16 |
Frozen to one exact build. Never changes. | You want to decide when to move up, after reading the release notes. |
For maximum reproducibility you can pin by digest, which is immune even to a tag being re-pushed:
docker pull 60plus/gamesdownloader:1.0.16
docker inspect --format='{{index .RepoDigests 0}}' 60plus/gamesdownloader:1.0.16
# -> 60plus/gamesdownloader@sha256:.... put that in image:Check which version an image is once it is running:
curl http://localhost:8080/api/health # {"status":"ok","version":"1.0.16"}Note on
PUID/PGID: the prebuilt image honours these exactly like a source build - the entrypoint drops privileges at start. Nothing extra is needed.
Building locally produces the same image from the code in the repository. Choose this when you want to modify the code, run on a machine that cannot reach Docker Hub (air-gapped or restricted networks), audit exactly what goes into the image, or develop your own plugins/themes against a local build.
1. Leave the build: block in docker-compose.yml as it ships (the default). Then:
docker compose build gamesdownloader
docker compose up -dWhat the build does. It is a multi-stage Docker build, so the final image stays lean:
-
Frontend stage (
node:22-alpine) - runsnpm ciandnpm run buildto compile the Vue 3 + Vite single-page app into static files. -
EmulatorJS stage (
alpine:3.20) - downloads a pinned, checksum-verified EmulatorJS release and unpacks it, so emulation is served from your own server rather than a third-party CDN. -
Backend stage (
python:3.13-slim) - installs system packages (ClamAV, Transmission,ffmpeg, Node.js for the plugin compiler), installs the Python requirements, then copies in the backend code, the compiled frontend, and EmulatorJS.
How long / how much. Expect a few minutes on a typical machine (dominated by the npm install and the Python wheel install). Subsequent builds are much faster because Docker caches the dependency layers; only the layers after your changed files rebuild. Give the Docker daemon at least ~2 GB of free RAM and a few GB of disk for the build cache.
Rebuilding after you pull code changes:
git pull
docker compose build gamesdownloader
docker compose up -d gamesdownloaderOptional build arguments. The EmulatorJS version is pinned in the Dockerfile via build args (EMULATORJS_VERSION, EMULATORJS_SHA256); override them only if you know you need a different EmulatorJS release and have the matching checksum.
Multi-architecture builds. A plain docker compose build produces an image for your host's architecture only. To build for another platform (for example an arm64 image on an amd64 machine), use Docker Buildx:
docker buildx build --platform linux/arm64 -t gamesdownloader:arm64 .If a from-source build fails with an odd decompression or segmentation error partway through, it is almost always a corrupted Docker layer cache rather than a code problem. See Troubleshooting & FAQ for the clean-rebuild procedure.
| Option A (Docker Hub) | Option B (build from source) | |
|---|---|---|
| Setup speed | Fast (pull only) | Slower (compiles frontend + installs deps) |
| Host toolchain | None | Docker with enough RAM/disk for the build |
| Needs internet to Docker Hub | Yes | No (once the base images are cached) |
| Code modifications | No | Yes |
| Recommended for | Almost everyone | Contributors, air-gapped hosts, auditors |
Open .env. At minimum, change every password before the instance is reachable by anyone but you. Here is every variable:
| Variable | Default | What it does |
|---|---|---|
GD_BASE_DIR |
. |
Host directory under which all persistent data is stored. The default . puts a data/ folder next to your compose file. Set an absolute path (e.g. /opt/gamesdownloader or /mnt/storage/games) to use a larger disk. |
GD_PORT |
8080 |
Host port mapped to the container's web UI. |
PUID / PGID
|
empty (root) | Optional. Run the container's processes as a specific user/group so files on the host are owned by you, not root. See below. |
GD_DEBUG |
false |
Verbose logging and relaxed checks. Never enable in production - among other things it downgrades the secret-key safety guard. |
| Variable | Default | What it does |
|---|---|---|
DB_NAME |
gamesdownloader |
Database name. |
DB_USER |
gd |
Application database user. |
DB_PASSWD |
change-me-strong-random |
Password for DB_USER. Change it.
|
DB_ROOT_PASSWD |
change-me-root-strong-random |
MariaDB root password. Change it. |
Important:
DB_ROOT_PASSWDis applied once, on the very first start of the MariaDB container, and baked into its data volume. Changing it in.envlater does not change it in an existing database. Set a strong value before the first start.
Generate strong values with:
openssl rand -hex 24| Variable | Default | What it does |
|---|---|---|
REDIS_PASSWORD |
change-me-redis-strong-random |
Password for the Valkey server (used for sessions, brute-force counters, job queue). Change it. |
| Variable | Default | What it does |
|---|---|---|
GD_AUTH_SECRET_KEY |
change-me-in-production |
Signs JWT session tokens and derives the key that encrypts secrets at rest (SMTP password, OAuth client secrets, scraper API keys). |
Behaviour:
- If you leave it at
change-me-in-productionor empty, the container auto-generates a 256-bit key on first start and saves it to/data/config/.secret_key(inside a mounted volume, so it survives rebuilds). This is the recommended path for most users. - If you set your own, use a long random string:
openssl rand -hex 32. -
Keep
/data/config/.secret_keysafe. Losing it invalidates every session and makes encrypted-at-rest secrets unrecoverable (you would have to re-enter your scraper keys and SMTP password). - A known-weak or empty key is refused at startup - the server will not sign tokens with an insecure secret.
Enter scraper credentials in the app, not in
.env. GamesDownloader reads these keys from its own settings, where they are encrypted at rest (the encryption key is derived from yourGD_AUTH_SECRET_KEY). Add them in the setup wizard on first run, or any time under Settings → Metadata. Values entered there are never written to disk in plaintext.GamesDownloader does not read scraper credentials from environment variables at all, which is why the compose and
.envabove do not listIGDB_CLIENT_ID,RAWG_API_KEY,SCREENSCRAPER_USERNAMEor the like. Putting a secret in.envwould store it in plaintext on the host and expose it to anyone who can rundocker inspect, with no benefit.
The credentials you can configure (all optional) and where each is used:
| Setting (in the UI) | Service |
|---|---|
| IGDB client ID + secret | IGDB (via a Twitch developer app) |
| RAWG API key | RAWG.io |
| SteamGridDB API key | SteamGridDB |
| ScreenScraper username + password | ScreenScraper.fr |
| RetroAchievements API key | RetroAchievements (optional) |
See Scrapers & Metadata for where to obtain each one and how title matching works.
Uncomment in .env to override defaults:
# GD_TOKEN_EXPIRE_MIN=60 # access-token lifetime in minutes
# GD_REFRESH_EXPIRE_DAYS=7 # refresh-token lifetime in daysdocker compose up -dThis starts three containers:
| Container | Image | Role |
|---|---|---|
GamesDownloader |
built or 60plus/gamesdownloader
|
The application (web UI, API, workers, ClamAV, Transmission). |
gd-mariadb |
mariadb:11.3 |
Database. |
gd-redis |
valkey/valkey:8 |
Cache, sessions, brute-force counters, job queue. |
The app waits for MariaDB and Valkey to report healthy before it starts (Compose depends_on with health conditions), so a clean up may take a moment before the app begins initialising.
The first start does more work than later ones, and it is normal for the web UI to be unavailable for 60-120 seconds:
- The database schema is created and migrated.
- The app starts downloading the LaunchBox metadata archive (~150 MB) and building an on-disk SQLite index from it (used for ROM cover art and metadata). This runs as a background task - it does not block the API from binding, so the web UI comes up without waiting for it. Until the index has finished building, the first ROM metadata search may be slow.
- ROM platform directories are created under
data/games/roms/. - If
GD_AUTH_SECRET_KEYwas left at its default, a secret key is generated and written todata/config/.secret_key.
The container's health check uses a 120 second start_period as a safety margin, so the orchestrator does not mark it unhealthy while the first-boot work (schema migration, directory setup) settles. It is not waiting on LaunchBox: /api/health returns as soon as uvicorn binds and never touches the LaunchBox index. If you watch docker compose logs -f gamesdownloader you will see a line indicating the API is listening on port 8080, with the LaunchBox index still building in the background.
| 1. Welcome and language | 2. Create admin account |
![]() |
![]() |
| 3. Connect GOG (optional) | 4. Metadata scrapers |
![]() |
![]() |
| 5. Email notifications (optional) | 7. Complete |
![]() |
![]() |

The language picker on the welcome step
Open http://localhost:8080 (or http://<server-ip>:8080 from another device on your LAN). On first run you are greeted by a 7-step setup wizard, no shell access required:
- Welcome - pick your interface language (8 available).
- Admin account - create the first administrator (username, email, password).
- GOG account - optionally connect a GOG account for library sync (can be done later in Profile).
- Scrapers - enter IGDB, RAWG, ScreenScraper, SteamGridDB keys (all optional, changeable later).
- Email - optionally configure an SMTP server for notifications and security alerts (changeable later in Settings).
- App settings - a single toggle controlling whether new users may self-register (public sign-up). Off by default, and changeable later in Settings.
- Complete - the wizard writes everything and drops you into the app.
Everything chosen here can be changed afterwards in Settings. The wizard only appears while no admin account exists.
A few features need a one-time nudge after install:
- ClamAV antivirus (optional): go to Settings → Security → ClamAV and click Update Definitions to download the virus database (~300 MB). The ClamAV daemon starts automatically once definitions are present. Until then, upload/download scanning is simply inactive - the app runs fine without it.
-
Transmission / torrents (optional): the Transmission daemon starts inside the container automatically. Enable it in Settings → Downloads → Transmission. If you want inbound peer connections, expose ports
9091(WebUI) and51413(peers) through your firewall. -
ROMs: place ROM files in
GD_BASE_DIR/data/games/roms/{platform}/(for exampleroms/snes/,roms/n64/), then use Scan ROMs in the Emulation library. See ROMs & Emulation.
Everything persistent sits under GD_BASE_DIR on the host. The most important directories:
| Path | Contents |
|---|---|
data/games/GOG |
Published GOG game files |
data/games/CUSTOM |
Custom game files (uploaded, torrent, scanned) |
data/games/roms/{platform} |
ROM files per platform |
data/downloads |
GOG installer staging (pre-publish) |
data/resources/... |
Downloaded artwork, save states, avatars, platform art |
data/config |
settings.yaml, the .secret_key, LaunchBox index, Transmission config |
data/plugins |
Installed plugins, one subdirectory each |
data/clamav |
ClamAV virus definitions |
data/db |
MariaDB data files |
data/redis |
Valkey persistence |
The full volume table (including per-user save-state and media paths) is on the Configuration page. Back up data/db and data/config at minimum - the database plus the secret key are what make your library and encrypted settings recoverable.
By default the container runs as root, so files it writes on the host are root-owned. To have them owned by your own user, find your IDs on the host:
id -u # -> PUID
id -g # -> PGIDPut those into .env:
PUID=1000
PGID=1000Then recreate the app container (docker compose up -d). New files will be owned by that user/group. If you switch this on an existing install, you may need to chown -R the GD_BASE_DIR once so previously root-owned files are readable.
docker compose pull gamesdownloader
docker compose up -d gamesdownloadergit pull
docker compose build gamesdownloader
docker compose up -d gamesdownloaderDatabase schema migrations run automatically on start. Your data volumes are untouched by an image rebuild. It is good practice to back up data/db before a major upgrade.
Tip: pin a specific image tag (e.g.
60plus/gamesdownloader:1.0.16) instead oflatestif you prefer to review release notes before each upgrade.
The app exposes a health endpoint that also reports the running version:
curl http://localhost:8080/api/health
# {"status":"ok","version":"1.0.16"}Check container status and logs:
docker compose ps
docker compose logs -f gamesdownloaderA healthy stack shows all three containers Up and the app (healthy) once the first-boot window has passed.
To stop the stack but keep your data:
docker compose downTo remove the containers and delete all data (this is destructive and irreversible):
docker compose down
rm -rf "$GD_BASE_DIR" # deletes the database, library, artwork, everythingTo reset just the application state (fresh setup wizard) while keeping your game files, remove data/db and data/redis but leave data/games in place. Note that this also discards users, settings, and scraped metadata.
Next: Configuration for the full settings tour, or Reverse Proxy & HTTPS if you plan to expose the instance beyond your LAN.
Getting Started
Configuration
Features
- Dashboard
- Library
- Collections
- Game Requests
- GOG Integration
- ROMs & Emulation
- Downloads & Torrents
- Users & Permissions
- Themes
Plugin Development
Reference





