Motrix Server (Docker)

Motrix Server packages the Motrix download core and aria2 into a non-root, multi-architecture container image for NAS boxes and home servers. It is the same download engine, task model, and settings as the desktop app — only the Electron shell is gone. You reach it from a browser instead.

Tagged releases publish the identical image to both registries:

  • Docker Hub — docker.io/motrixapp/motrix-server
  • GitHub Container Registry — ghcr.io/agalwood/motrix-server

Every release image contains linux/amd64 and linux/arm64, and Docker picks the matching manifest for you. 32-bit ARM is not supported.

What the container exposes

Server mode publishes two separate services by default on two ports. Both are HTTP, and they are not interchangeable. Raw aria2 RPC is a third, independent endpoint that stays on container loopback unless you explicitly opt in later on this page.

AddressWhat it serves
http://NAS_HOST:8080Web UI, operator API, and the public GET /healthz probe
http://NAS_HOST:16801MDXP endpoint — unary POST /mdxp, the event stream GET /mdxp/events, device-code pairing for CLI clients and agents, and the opt-in MBP1 routes for browser extensions

Important

A remote browser extension uses MDXP over MBP1. It does not reuse a URL token from an old beta Server or an aria2 RPC token. When upgrading an existing Server, update Motrix Server and the extension together, remove the old Server entry from the extension, then pair again with the new WS/WSS address. An older Server without the remote MBP1 routes normally returns 404 for /discovery or /nonce; changing the address to port 8080 will not fix that.

Before you start

  • A 64-bit host (amd64 or arm64) with Docker and the Compose plugin.
  • Two directories on persistent storage: one for state, one for downloads. Mount them at /data and /downloads.
  • Both directories owned by the numeric UID/GID the container runs as — 1000:1000 unless you override it.

Warning

/data holds the SQLite database, settings, aria2 session and DHT state, torrent metadata, the operator token, and installed plugins. Recreating the container without that mount loses all of it. Back up /data; back up /downloads according to your own policy.

Quick start with Docker Compose

Use the compose.yaml shipped in the repository:

name: motrix

services:
  server:
    image: "${MOTRIX_IMAGE:-motrixapp/motrix-server:latest}"
    init: true
    read_only: true
    user: "${MOTRIX_UID:-1000}:${MOTRIX_GID:-1000}"
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    stop_grace_period: 2m
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m,mode=1777
    environment:
      MOTRIX_DATA_DIR: /data
      MOTRIX_TEMP_DIR: /data/tmp
      MOTRIX_PLUGIN_DIR: /data/plugins
      MOTRIX_DEFAULT_SAVE_DIR: /downloads
      MOTRIX_ALLOWED_SAVE_DIRS: /downloads
      MOTRIX_ARIA2_RPC_LISTEN_ALL: "${MOTRIX_ARIA2_RPC_LISTEN_ALL:-false}"
      MOTRIX_MDXP_HOST: 0.0.0.0
      MOTRIX_MDXP_PORT: 16801
      MOTRIX_PUBLIC_URL: "${MOTRIX_PUBLIC_URL:-}"
      MOTRIX_REMOTE_EXTENSION_ENABLED: "${MOTRIX_REMOTE_EXTENSION_ENABLED:-false}"
      MOTRIX_REMOTE_EXTENSION_PUBLIC_URL: "${MOTRIX_REMOTE_EXTENSION_PUBLIC_URL:-}"
      MOTRIX_ALLOW_INSECURE_OPERATOR_HTTP: "${MOTRIX_ALLOW_INSECURE_OPERATOR_HTTP:-false}"
    ports:
      - "${MOTRIX_WEB_BIND_IP:-${MOTRIX_BIND_IP:-0.0.0.0}}:${MOTRIX_HTTP_PORT:-8080}:8080"
      - "${MOTRIX_MDXP_BIND_IP:-${MOTRIX_BIND_IP:-0.0.0.0}}:${MOTRIX_MDXP_PUBLIC_PORT:-16801}:16801"
    volumes:
      - ./motrix-data:/data
      - ./downloads:/downloads

Then prepare the directories and start the service:

mkdir -p motrix-data downloads

# Use a dedicated non-root account; these commands use the current user.
export MOTRIX_UID="$(id -u)"
export MOTRIX_GID="$(id -g)"
chown "$MOTRIX_UID:$MOTRIX_GID" motrix-data downloads

export MOTRIX_PUBLIC_URL='http://nas.example.lan:8080'
docker compose pull server
docker compose up -d --wait
docker compose ps

MOTRIX_IMAGE selects the registry, tag, or digest without editing the file — for example ghcr.io/agalwood/motrix-server:2.0.0, or a @sha256: digest for a fully reproducible deployment. Floating tags such as :latest are convenient on a NAS; an immutable SemVer tag or digest is what you want for controlled upgrades and rollback.

Caution

Never set the runtime UID to 0 or enable privileged mode to work around a mount permission error. Fix the ownership of the two directories instead. Startup write-tests every path it needs and fails with the exact absolute path when one is wrong.

The same deployment with docker run

docker run -d \
  --name motrix-server \
  --init \
  --restart unless-stopped \
  --stop-timeout 120 \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 \
  --security-opt no-new-privileges:true \
  --user "$(id -u):$(id -g)" \
  -e MOTRIX_PUBLIC_URL='http://nas.example.lan:8080' \
  -e MOTRIX_MDXP_HOST=0.0.0.0 \
  -p 8080:8080 \
  -p 16801:16801 \
  -v "$PWD/motrix-data:/data" \
  -v "$PWD/downloads:/downloads" \
  motrixapp/motrix-server:latest

The image already defines its own health check, non-root user, data paths, and graceful SIGTERM handling. MOTRIX_MDXP_HOST=0.0.0.0 is the listener inside the container; the -p options decide which host interfaces can reach it.

Raw aria2 RPC opt-in

The aria2 engine RPC endpoint is separate from the Motrix Web/API and MDXP services. It stays on loopback (127.0.0.1) inside the container by default. Keep that default for normal Web, CLI, and agent use. Browser integrations that support MDXP should prefer it over raw aria2 RPC.

Set MOTRIX_ARIA2_RPC_LISTEN_ALL=true only when a trusted external aria2 RPC client must reach the engine directly. Before restarting, set a non-empty RPC secret and verify the RPC port in Settings → Advanced. Server mode refuses all-interface RPC listening when the secret is blank.

The Compose example above passes the opt-in variable but intentionally does not publish port 16800. After enabling the opt-in, a client in the same Compose network can connect to http://server:16800/jsonrpc. For a client on the Docker host, save this explicit override as compose.aria2-rpc.yaml:

services:
  server:
    ports:
      - "127.0.0.1:16800:16800"

Then enable both the listener and port publication and recreate the service:

export MOTRIX_ARIA2_RPC_LISTEN_ALL=true
docker compose -f compose.yaml -f compose.aria2-rpc.yaml up -d --wait

With docker run, add -e MOTRIX_ARIA2_RPC_LISTEN_ALL=true and -p 127.0.0.1:16800:16800 instead. To connect from a trusted LAN, replace the host-side 127.0.0.1 with the NAS’s specific LAN address and allow only the required source in the host firewall. If you changed the RPC port in Motrix settings, update both sides of the mapping. Configure the external client with the same RPC secret; aria2 sends it as a token:<secret> authentication parameter.

Warning

Raw aria2 RPC is not governed by Motrix operator authentication or download-path policy, and it has no TLS. Possession of the RPC secret effectively grants control of the download engine. Never publish it directly to the public internet; prefer a private Docker network, host loopback, VPN, or another authenticated encrypted tunnel.

First login: the operator token

On first start Motrix generates a random operator token at /data/operator-token, mode 0600. The same token survives restarts and image replacement.

With the bind-mount layout above, read it from the host. The printf adds a final line break for compatibility with older token files that do not end with one:

printf '%s\n' "$(cat motrix-data/operator-token)"

When using a named volume, or when the host directory is not directly available, read it inside the container:

docker compose exec server sh -c 'token=$(cat /data/operator-token); printf "%s\n" "$token"'

Note

Some shells, notably zsh, display a % after output that did not end with a line break. That % is a terminal marker, not part of the operator token; do not paste it into the unlock field. Both commands above print only the token and finish with a normal line break for old and new token files.

Open http://NAS_HOST:8080. The web UI shows an Unlock Motrix screen with an Operator token field. Paste the token and click Unlock.

You can set MOTRIX_OPERATOR_TOKEN yourself instead, but environment variables are readable from container metadata — the generated file is the safer default on a single host.

Tip

If unlocking fails, re-read the current /data/operator-token. A token copied from another deployment will never work.

Environment variables you are likely to set

VariableImage defaultWhat it does
PORT8080Web/API listen port inside the container
MOTRIX_DATA_DIR/dataState directory; must be absolute and writable
MOTRIX_DEFAULT_SAVE_DIR/downloadsWhere new tasks save by default
MOTRIX_ALLOWED_SAVE_DIRS/downloadsColon-separated list of absolute roots tasks may write to, enforced server-side
MOTRIX_PUBLIC_URLunsetThe externally reachable web approval URL handed to pairing clients
MOTRIX_REMOTE_EXTENSION_ENABLEDfalseOpt in to the four remote MBP1 routes used by browser extensions
MOTRIX_REMOTE_EXTENSION_PUBLIC_URLunsetExact WS/WSS Server address entered in the extension; it may include a reverse-proxy base path
MOTRIX_ALLOW_INSECURE_OPERATOR_HTTPfalseExplicitly allow an HTTP operator page on a trusted LAN only; never enable it on an untrusted network
MOTRIX_OPERATOR_TOKENgenerated fileOperator credential, if you’d rather supply it than read the file
MOTRIX_ARIA2_RPC_LISTEN_ALLfalseOpt in to an authenticated all-interface aria2 RPC listener; publishing port 16800 is separate
MOTRIX_FFMPEG_PATHauto-detectAbsolute path to an FFmpeg binary you provide
LOG_LEVELinfoLog level written to container stdout

To add a second download root, mount it and allow it — both, or the task will be rejected:

environment:
  MOTRIX_ALLOWED_SAVE_DIRS: /downloads:/archive
volumes:
  - /srv/archive:/archive

The full environment reference — plugin sources, secret seed, bind addresses, MDXP listener details — is in the deployment guide linked at the end of this page.

Pairing the CLI and AI agents

The local-socket shortcut the desktop app uses does not exist here, so a remote motrix CLI or agent pairs over MDXP with a device code.

MOTRIX_PUBLIC_URL is the web approval URL returned to that client. It has no localhost default: set it to the URL other machines actually use — the web port (or its reverse-proxy URL), never the MDXP port, and never localhost, 127.0.0.1, or 0.0.0.0. Leaving it unset does not disable pairing, but the client has no useful link to show you.

Start pairing on the client, then approve it one of two ways:

  1. In the web UI — open Settings → Integration → Command-line tools. The request appears under Pending approvals with its verification code; click Approve (or Deny).
  2. Over SSH — approve the exact code from inside the running container:
docker compose exec server motrix-admin pairing pending
docker compose exec server motrix-admin pairing approve ABCD-EFGH
docker compose exec server motrix-admin pairing deny ABCD-EFGH

motrix-admin talks to the running server over container loopback only, and never prints the operator credential or the client token. It deliberately has no approve-latest, approve-all, or remote endpoint — you always type the code the client showed you. Web approval remains the normal path; this is the recovery path for headless deployments.

Pair a remote browser extension (MBP1)

Remote extension pairing is off by default. The two addresses that are easiest to confuse have different jobs:

  • MOTRIX_PUBLIC_URL is the web approval address opened in a browser, normally port 8080 or its HTTPS reverse-proxy URL.
  • MOTRIX_REMOTE_EXTENSION_PUBLIC_URL is the Server address entered in the extension. A direct connection normally uses MDXP/MBP1 port 16801, not web port 8080.

For a direct connection on a trusted LAN, explicitly allow the HTTP operator page and use WS:

export MOTRIX_REMOTE_EXTENSION_ENABLED=true
export MOTRIX_REMOTE_EXTENSION_PUBLIC_URL='ws://nas.example.lan:16801'
export MOTRIX_PUBLIC_URL='http://nas.example.lan:8080'
export MOTRIX_ALLOW_INSECURE_OPERATOR_HTTP=true
docker compose up -d --wait
docker compose logs server

When the service is ready, its log includes a line like this. Copy the address exactly into the extension instead of guessing the port:

Motrix Extension pairing ready. Enter this Server address in the Extension: ws://nas.example.lan:16801

WS is allowed: MBP1 still encrypts application content and authenticates a previously paired Server instance. WS does not add TLS protection for connection metadata or the Server identity during first pairing, however, and the HTTP operator page exposes the operator token, pairing codes, cookies, and administration traffic to an on-path attacker. Use this configuration only on a LAN you fully trust.

For the Internet, an untrusted LAN, or TLS Server identity, terminate TLS at a trusted reverse proxy:

export MOTRIX_REMOTE_EXTENSION_ENABLED=true
export MOTRIX_REMOTE_EXTENSION_PUBLIC_URL='wss://motrix.example.com/bridge'
export MOTRIX_PUBLIC_URL='https://motrix.example.com'
unset MOTRIX_ALLOW_INSECURE_OPERATOR_HTTP
docker compose up -d --wait

The reverse proxy must forward /bridge/discovery, /bridge/nonce, /bridge/pair, and /bridge/v1 to http://127.0.0.1:16801 without stripping /bridge. Preserve Host, Origin, Upgrade, Connection, and Sec-WebSocket-Protocol. Firewall origin ports 8080 and 16801 so that only the reverse proxy can reach them.

Add that Server in the extension and start pairing. The Motrix web operator UI shows the request and an eight-character pairing code. Verify the browser and extension identity, then enter that code in the extension to authenticate. Deny the request in Motrix if you did not start it. After pairing, the extension scopes its credential to that Server address and authenticated instance. Pair again after changing the scheme, host, port, base path, or Server instance. MBP1 v1 has no legacy-token fallback or protocol downgrade.

Security boundaries

The ordinary web UI may use HTTP on a trusted LAN. When remote browser-extension support is enabled and MOTRIX_PUBLIC_URL uses HTTP, you must also set MOTRIX_ALLOW_INSECURE_OPERATOR_HTTP=true. This acknowledges the risk; it does not add encryption.

Warning

Do not expose either port to the internet as plain HTTP. Internet access requires TLS termination at a trusted reverse proxy and a firewall in front of the origin ports — for both services. Forwarding only 8080 does not publish MDXP; forwarding only 16801 does not serve the approval UI. Configure the proxy to preserve cookies, authorization headers, and streaming responses.

Never implement that protection by disabling pairing. Remote CLI and agent pairing stays an operator-approved workflow.

The container itself is hardened by default: it runs as a non-root user with a read-only root filesystem, no-new-privileges, and a small noexec tmpfs for /tmp. Keep it that way — do not mount the Docker socket or hand the container your whole filesystem.

How server mode differs from the desktop app

Note

The web UI shows “This web edition is updated by its deployment administrator.” in Settings → About. There is no in-app update button: you upgrade by pulling a new image and recreating the container. Back up /data first, record the digest you are replacing, and read the release notes before crossing a major version.

Other honest differences:

  • Browser-extension pairing uses a different path. The desktop app discovers local Motrix through native messaging. Server mode keeps its remote entry point off by default and, when enabled, pairs through MDXP over MBP1. Both require user approval, but their credentials and trust scopes are separate. See Browser extension.
  • No FFmpeg in the official image. A plugin that needs it requires a derived image with Alpine’s ffmpeg package plus MOTRIX_FFMPEG_PATH=/usr/bin/ffmpeg.
  • One-click registry install is coming soon in the web build. You can browse the plugin directory, but for now install by uploading a .moext package in the UI, or declare sources with MOTRIX_PLUGIN_INSTALL_URLS / MOTRIX_PLUGIN_IMPORT_DIRS at startup. Installed packages, grants, configuration, and secrets persist under /data and survive a container replacement. See Plugins.

Everything else — HTTP and BitTorrent tasks, speed limits, trackers, proxies — behaves as documented in the rest of this manual.

NAS platforms

Synology DSM 7 Container Manager and fnOS both import compose.yaml as a project. The shape is the same on either: create the two directories on a real storage pool, give them to a dedicated non-administrator account’s numeric UID/GID, set MOTRIX_UID / MOTRIX_GID to match, set MOTRIX_PUBLIC_URL to the URL your other devices will use, then start the project and wait for the health status before opening the web UI. Do not enable “high privilege” and do not let the NAS build a local image — it should pull the published one. Step-by-step instructions for both platforms are in the deployment guide.

Next steps

  • Motrix CLI — install @motrix/cli and pair it with this server.
  • Plugins — what plugins can do and how to install them.
  • Troubleshooting — when a download won’t start.

For image signing and provenance, tag policy, named volumes, reverse-proxy topologies, upgrade and rollback procedure, /api/diagnostics, the DSM and fnOS walkthroughs, and the complete environment table, read the full deployment guide.