Run a 24/7 aria2 download server with Docker
A complete, accurate deployment you fully control: an aria2 engine in a small Alpine container, a proper aria2.conf, optional AriaNg WebUI, secure remote access, post-download automation and health checks. Works on any Linux host, Synology, QNAP, Unraid or a VPS.
Last reviewed 2026-09 · aria2 v1.37.0 · Docker Engine 24+ / Compose v2
Before you start
- Docker Engine and Compose v2 (
docker compose version). - A host with a few GB free — a VPS, mini-PC, Raspberry Pi, or NAS.
- Optional: a domain and a reverse proxy for HTTPS remote access.
- Optional but recommended: forward one BitTorrent port (TCP+UDP) on your router for good peer connectivity.
Why Docker, and what we build
Running aria2 in a container gives you an isolated, reproducible, always-on downloader you can update or move without touching the host. The stack has three independent parts:
1. Engine (aria2c)
The multi-protocol downloader, reading a mounted aria2.conf and exposing JSON-RPC on port 6800.
2. WebUI (optional)
AriaNg, or the online WebUI, talks to the engine over RPC. Fully replaceable.
3. Storage
Two mounts: /downloads (payload) and /config (session, logs, hooks) — both persist outside the container.
We build our own tiny image from alpine. That keeps the guide accurate and portable — no hidden env variables from a third-party image you can't inspect. (Community images exist if you prefer a shortcut; verify their variables against their own docs.)
The three files
Create a folder (e.g. /opt/aria2) and add these three files next to each other.
1. aria2.conf
dir=/downloads continue=true file-allocation=falloc max-concurrent-downloads=5 max-connection-per-server=16 min-split-size=10M split=16 # --- RPC (required for any WebUI) --- enable-rpc=true rpc-listen-all=true rpc-listen-port=6800 rpc-secret=CHANGE_ME_STRONG_SECRET # needed by browser WebUIs; see security notes rpc-allow-origin-all=true # --- BitTorrent --- bt-max-peers=128 # listen ports must match the ports published in docker-compose.yml listen-port=6888 dht-listen-port=6888 enable-dht=true enable-peer-exchange=true bt-enable-lpd=true seed-ratio=1.0 disk-cache=64M # --- Session persistence (survives restarts) --- save-session=/config/aria2.session input-file=/config/aria2.session save-session-interval=60 log=/config/aria2.log
2. Dockerfile
FROM alpine:3.20 RUN apk add --no-cache aria2 ca-certificates tzdata bash COPY aria2.conf /etc/aria2/aria2.conf VOLUME ["/downloads", "/config"] EXPOSE 6800 6888 6888/udp # aria2 refuses to start if input-file is missing, so create the session file first ENTRYPOINT ["sh", "-c", "touch /config/aria2.session && exec aria2c --conf-path=/etc/aria2/aria2.conf"]
3. docker-compose.yml
services:
aria2:
build: .
container_name: aria2
restart: unless-stopped
environment:
- TZ=UTC
volumes:
- ./aria2-config:/config
- ./downloads:/downloads
ports:
- "6800:6800" # JSON-RPC (keep private!)
- "6888:6888" # BitTorrent TCP
- "6888:6888/udp" # BitTorrent DHT UDP
The engine writes aria2.session and aria2.log into ./aria2-config, so both live on the host and persist across restarts and image rebuilds.
Deploy in 5 minutes
mkdir -p /opt/aria2/aria2-config /opt/aria2/downloads cd /opt/aria2 # save aria2.conf, Dockerfile and docker-compose.yml here docker compose up -d --build # build once, then start docker compose ps # confirm it is running docker compose logs -f aria2 # watch startup
Confirm the RPC endpoint answers (replace the secret):
curl -s http://127.0.0.1:6800/jsonrpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"aria2.getVersion","params":["token:CHANGE_ME_STRONG_SECRET"]}'
A JSON reply with a version field means the engine and secret are correct.
Configure & secure
| Setting | Why |
|---|---|
rpc-secret | Required. Without it anyone who reaches port 6800 controls your downloads. |
rpc-allow-origin-all=true | Lets a browser WebUI (AriaNg, this site's WebUI) call the RPC. Only safe because the port stays private. |
file-allocation=falloc | Reserves space quickly and reduces disk fragmentation (use none on filesystems that don't support it). |
disk-cache=64M | Batch writes in RAM — fewer micro-writes, longer disk life. |
continue=true | Resume partial downloads across restarts. |
save-session / input-file | Persist the queue; downloads resume automatically after a restart. |
Never publish port 6800 to the internet. Keep it bound to the host/LAN and reach it through a VPN or an authenticated reverse proxy (see below).
Add a WebUI
Pick whichever fits your workflow:
- Nothing to install: open the online WebUI in any browser and connect to your server's RPC (host, port
6800, your secret). - Self-hosted AriaNg: serve the AriaNg static files with nginx and point it at the RPC. Add this service to the compose file, after downloading an AriaNg release into
./ariang:
ariang:
image: nginx:alpine
container_name: ariang
restart: unless-stopped
volumes:
- ./ariang:/usr/share/nginx/html:ro
ports:
- "6880:80"
In AriaNg's settings, set RPC host to your server, port 6800, and paste the rpc-secret.
NAS & platform notes
Linux / VPS / Raspberry Pi
Follow the 5-minute deploy. On a Pi, keep disk-cache modest and expect lower throughput.
Synology DSM 7+
Container Manager → Project → Create; paste the compose. Use real DSM paths for the mounts (e.g. /volume1/docker/aria2/config and /volume1/downloads).
QNAP / Unraid
Container Station → Create Application, or Unraid's Compose plugin. Map /config to an appdata path and /downloads to a share.
File ownership
If other apps (Plex/Jellyfin/Arr) must read the files, either set the container user or chown the download folder. Our Alpine image runs as root by default; add user: "1000:1000" to the service to match your host UID/GID.
Secure remote access (HTTPS reverse proxy)
To reach the WebUI over the internet, put an authenticated HTTPS proxy in front and never expose 6800 directly. Modern browsers also block unencrypted RPC from an HTTPS page (mixed content), so you need TLS + WebSocket forwarding.
server {
listen 443 ssl;
server_name aria2.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
auth_basic "aria2";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://127.0.0.1:6880; # AriaNg
proxy_set_header Host $host;
}
location /jsonrpc {
proxy_pass http://127.0.0.1:6800/jsonrpc;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400; # keep WebSocket alive
}
}
Layer security: HTTPS, an auth layer (basic auth or an identity proxy), a strong rpc-secret, and a firewall. A VPN (WireGuard/Tailscale) is the simplest way to avoid exposing the service at all.
Post-download automation
aria2 runs a command when a download finishes via on-download-complete. Add it to aria2.conf and mount the script:
# in aria2.conf on-download-complete=/config/on-complete.sh on-download-error=/config/on-error.sh # /config/on-complete.sh (chmod +x) #!/bin/bash GID="$1"; NUM_FILES="$2"; TARGET_PATH="$3" # $3 is the first file's path logger "aria2 finished $TARGET_PATH" # Examples: # rclone move "$(dirname "$TARGET_PATH")" remote:downloads/ # unzip -o "$TARGET_PATH" -d "$(dirname "$TARGET_PATH")" # curl -s "https://ntfy.sh/mytopic" -d "Download complete: $(basename "$TARGET_PATH")"
$1 = GID, $2 = number of files, $3 = path of the first file. Keep hooks fast and non-blocking; do the heavy work in the background.
Health & monitoring
- Logs:
docker compose logs -f aria2(aria2 also writes/config/aria2.log). - Is it alive? call
aria2.getVersionover JSON-RPC (see Deploy) or checkdocker compose ps. - Add a healthcheck to the compose service so Docker can restart a wedged engine:
healthcheck:
test: ["CMD-SHELL", "aria2c --version >/dev/null && wget -qO- http://127.0.0.1:6800/jsonrpc --post-data='{\"jsonrpc\":\"2.0\",\"id\":\"h\",\"method\":\"aria2.getVersion\",\"params\":[\"token:CHANGE_ME_STRONG_SECRET\"]}' >/dev/null"]
interval: 60s
timeout: 10s
retries: 3
Troubleshooting
WebUI says "Disconnected" / RPC fails
Check three things: the container is running (docker compose ps), port 6800 is mapped, and the secret matches rpc-secret. If the WebUI loads over HTTPS, its RPC calls must also be HTTPS/WSS (mixed-content rule) — use the reverse proxy above.
Torrent shows 0 peers / DHT 0 nodes
Map and forward one port for TCP and UDP (we use 6888), and add live trackers with the tracker list or bt-tracker=… in the config. DHT bootstraps once it can reach the network.
Permission denied writing downloads
The container writes as its user (root by default). Either set user: "1000:1000" to match the host, or chown the ./downloads folder so the container user can write to it.
Downloads restart from 0 after a reboot
Session persistence is not configured. Ensure save-session, input-file, and save-session-interval point at a mounted path (here /config/aria2.session) and that continue=true.
Port 6800 already in use / container won't start
Another aria2 instance or app owns 6800 on the host. Change the host mapping (e.g. "6801:6800") or stop the other process.
FAQ
Do I need a WebUI container?
No. AriaNg, this site's online WebUI, or any RPC client can talk to the engine directly. The WebUI is optional decoration on top of the RPC API.
How do I update aria2?
Rebuild the image when a new Alpine package is available: docker compose build --no-cache && docker compose up -d. Your config, session and downloads persist in the mounts.
Is a community image like p3terx/aria2-pro fine?
Yes if you validate its documented variables. This guide builds its own minimal image so the configuration is explicit and portable, but a maintained community image is a valid shortcut.
Related resources
- Online WebUI — manage this server from any browser
- BitTorrent trackers — keep swarms healthy
- Options reference — every aria2.conf key used here
- Troubleshooting — common aria2 errors with exact fixes
- Full aria2 documentation