Skip to content
Domekologe edited this page Aug 4, 2026 · 5 revisions

Docker

🌐 English · Deutsch

Official image: ghcr.io/pd-codes/mediaforge:latest (Debian-based, includes FFmpeg, mpv, Xvfb and Patchright Chromium for captchas).

Warning

NAS devices (UGREEN, Synology): Both are supported, but MediaForge's code is not optimized for these two NAS platforms and will not be — so expect recurring issues (Docker permissions, storage/DSM quirks, the captcha browser). We're happy to help with installation, but we do not optimize MediaForge for UGREEN or Synology. For the smoothest experience, use a standard Linux, Windows or macOS Docker host.

docker-compose (recommended)

services:
  mediaforge:
    container_name: mediaforge
    image: ghcr.io/pd-codes/mediaforge:latest
    ports:
      - "8080:8080"
    volumes:
      - ./Downloads:/app/Downloads          # downloads on the host
      - mediaforge-data:/home/mediaforge/.mediaforge  # configuration & database
      - mediaforge-tmp:/tmp/mediaforge      # scratch space (see note below)
    # security hardening
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:mode=1777,size=512m
      - /run:size=64m
    restart: unless-stopped

volumes:
  mediaforge-data:
  mediaforge-tmp:

Why the extra mediaforge-tmp volume: the /tmp tmpfs lives in RAM, so it stays deliberately small (512 MB) and only holds small files — log file, sockets, the captcha browser profile fallback. Everything that can grow to gigabytes is written to /tmp/mediaforge instead: yt-dlp/ffmpeg download and encode intermediates, and the player's HLS segments, which for a seekable stream keep the segments of the whole title until the session ends (up to 8 concurrent sessions). Without the volume all of that lands on the tmpfs, fills your RAM and breaks logging and the captcha browser along with the download. Give the volume room for the largest file you download plus roughly the source size per concurrent viewer.

Important: Create ./Downloads before the first start (mkdir -p Downloads) — otherwise Docker creates the folder as root and you get permission errors (the container runs as the unprivileged user mediaforge).

First-time setup

On first start the setup token appears in the container log:

docker compose logs -f mediaforge
# → open http://<HostIP>:8080/setup?token=<token>

Alternatively, create the admin account directly via environment variables (skips the setup page):

    environment:
      MEDIAFORGE_WEB_ADMIN_USER: "admin"
      MEDIAFORGE_WEB_ADMIN_PASS: "changeme"

Optional environment variables

All settings are configurable via the WebUI and stored in the database (volume mediaforge-data). Env variables are only useful for initial configuration/automation:

    environment:
      # SSO / OIDC (see Authentication)
      MEDIAFORGE_WEB_SSO: "1"
      MEDIAFORGE_WEB_FORCE_SSO: "1"
      MEDIAFORGE_OIDC_ISSUER_URL: "https://keycloak.example.com/realms/myrealm"
      MEDIAFORGE_OIDC_CLIENT_ID: "mediaforge"
      MEDIAFORGE_OIDC_CLIENT_SECRET: "secret"
      # reverse proxy
      MEDIAFORGE_WEB_BASE_URL: "https://mediaforge.example.com"

Technical details

  • The container runs mediaforge -wP 8080 -wN -wH 0.0.0.0 behind an Xvfb display (:99) — required so the captcha browser has a "real" display even headless.
  • Timezone/locale default to Europe/Berlin / de_DE.UTF-8 (overridable via TZ/LANG) — realistic values improve the captcha success rate.
  • Built-in healthcheck: verifies every 30 s that the WebUI responds.
  • Download path inside the container: /app/Downloads (preset via MEDIAFORGE_DOWNLOAD_PATH).

Reverse proxy (nginx, Traefik, …)

  1. Set MEDIAFORGE_WEB_BASE_URL to the public https:// URL (or in the UI under Settings) — this generates correct absolute URLs and enables secure cookies.
  2. Register https://<domain>/oidc/callback as the redirect URI at your OIDC provider.
  3. No WebSocket support needed; plain HTTP proxying is sufficient.

Routing through a VPN (Gluetun)

Some users route all of MediaForge's traffic through a Gluetun VPN container. MediaForge joins Gluetun's network namespace and inherits its kill switch — if the tunnel drops, MediaForge loses internet access too. A ready-to-use file for both variants below is in the repository: docker-compose.gluetun.yaml.

Whichever variant you use, the same two rules apply — and they are the usual stumbling blocks:

  1. The port is published on the Gluetun container, not on mediaforge. A container that joins another's network namespace may not declare its own ports: — Compose aborts with "conflicting options: port publishing and the container type network mode". Publish 8080 on gluetun instead.
  2. FIREWALL_OUTBOUND_SUBNETS must list your LAN subnet (e.g. 192.168.1.0/24). Otherwise Gluetun's firewall drops the reply packets and the WebUI stays unreachable from the local network — even though the container is running.

On startup MediaForge also logs a hint when it detects a VPN namespace (a wg0/tun0-style interface), reminding you about the port and firewall-subnet settings — visible via docker compose logs mediaforge. The image itself needs no changes — the built-in healthcheck (http://localhost:8080/) still works because localhost refers to the shared namespace.

Attach to an existing Gluetun (recommended)

The most common case: Gluetun already runs (in another stack or standalone) and you just want MediaForge to use it. Attach by container name with network_mode: "container:gluetun" — don't add a second gluetun service.

  1. On your existing gluetun container, publish MediaForge's port and allow your LAN subnet, then recreate gluetun:

        ports:
          - "8080:8080"
        environment:
          FIREWALL_OUTBOUND_SUBNETS: "192.168.1.0/24"
  2. Run MediaForge on its own, joining gluetun's namespace by name (replace gluetun with your container's actual name). container:<name> references a container across Compose projects but has no depends_on ordering — if gluetun is recreated, restart MediaForge afterwards so it re-joins the new namespace.

    services:
      mediaforge:
        container_name: mediaforge
        image: ghcr.io/pd-codes/mediaforge:latest
        network_mode: "container:gluetun"
        volumes:
          - ./Downloads:/app/Downloads
          - mediaforge-data:/home/mediaforge/.mediaforge
          - mediaforge-tmp:/tmp/mediaforge
        cap_drop:
          - ALL
        security_opt:
          - no-new-privileges:true
        read_only: true
        tmpfs:
          - /run:size=128m
          - /tmp:size=256m
        restart: unless-stopped
    
    volumes:
      mediaforge-data:
      mediaforge-tmp:

Full stack (Gluetun + MediaForge together)

Don't run Gluetun yet? Let Compose manage both in one file. Here MediaForge references the gluetun service with network_mode: "service:gluetun" (this only works within the same file) and can wait for it via depends_on: condition: service_healthy. Fill in the VPN_* / WIREGUARD_* values for your provider — the exact variable names differ per provider and are documented in the Gluetun wiki.

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "8080:8080"                          # MediaForge WebUI — published HERE
    environment:
      VPN_SERVICE_PROVIDER: "custom"
      VPN_TYPE: "wireguard"
      WIREGUARD_PRIVATE_KEY: "<your-private-key>"
      WIREGUARD_ADDRESSES: "10.0.0.2/32"
      FIREWALL_OUTBOUND_SUBNETS: "192.168.1.0/24"   # your LAN subnet — REQUIRED
      TZ: "Europe/Berlin"
    restart: unless-stopped

  mediaforge:
    container_name: mediaforge
    image: ghcr.io/pd-codes/mediaforge:latest
    network_mode: "service:gluetun"          # no "ports:" here!
    depends_on:
      gluetun:
        condition: service_healthy
    volumes:
      - ./Downloads:/app/Downloads
      - mediaforge-data:/home/mediaforge/.mediaforge
      - mediaforge-tmp:/tmp/mediaforge
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /run:size=128m
      - /tmp:size=256m
    restart: unless-stopped

volumes:
  mediaforge-data:
  mediaforge-tmp:

In docker-compose.gluetun.yaml the attach setup is active by default; this full stack is Option B at the bottom.

Updating

docker compose pull
docker compose up -d

Configuration and queue survive thanks to the mediaforge-data volume.

Clone this wiki locally