Files
aria2/PLAN.md
casjay 6b94fff43d
Some checks failed
aria2 / release-aria2 (push) Has been cancelled
🗃️ Removed the .claude/settings.local.json 🗃️
CLAUDE.md
Dockerfile
.env.scripts
.gitattributes
.gitea/workflows/docker.yaml
.gitignore
LICENSE.md
PLAN.md
README.md
rootfs/root/docker/setup/05-custom.sh
rootfs/tmp/etc/nginx/nginx.conf
rootfs/usr/local/bin/entrypoint.sh
rootfs/usr/local/bin/pkmgr
rootfs/usr/local/etc/docker/bin/
rootfs/usr/local/etc/docker/init.d/00-aria2c.sh
rootfs/usr/local/etc/docker/init.d/zz-nginx.sh
2026-05-12 20:05:30 -04:00

14 KiB

aria2 migration plan

Service intent

Self-hosted download daemon for HTTP, HTTPS, FTP, SFTP, BitTorrent, and Metalink. Single Alpine-based Docker image bundling aria2c (RPC daemon) + nginx + AriaNg (static web UI). aria2c runs with --enable-rpc --rpc-listen-all=true on port 6800 (JSON-RPC + WebSocket); nginx serves the AriaNg single-page app on port 80 and reverse-proxies /jsonrpc and /rpc to the local aria2c. Container exposes :80 (web UI + proxied RPC) and :6800 (direct RPC), with :6888 for BitTorrent peer/DHT traffic. Volumes: /config (user-editable aria2.conf, aria2.session, nginx.conf, AriaNg client config) and /data (downloads under /data/downloads/aria2, logs under /data/logs/{aria2,nginx}, runtime state).

Service stack

  • Download daemon: aria2 Alpine package -> /usr/bin/aria2c. Started by 00-aria2c.sh with --conf-path=/config/aria2/aria2.conf. Reads + writes session at /config/aria2/aria2.session (resume on restart). Logs to /data/logs/aria2/aria2.log.
  • Web UI: AriaNg 1.3.13 (mayswind/AriaNg GitHub release) — pure static HTML/JS/CSS, no backend. Pre-bundled in repo at rootfs/tmp/ariang-src/AriaNg-1.3.13.zip (1.1 MB; gitignored) so the build does not need to fetch from github.com inside buildx (which lacks reliable SSL egress on this host). Extracted at build time to /usr/local/share/ariang/.
  • Web server: nginx Alpine package -> /usr/sbin/nginx. Started by zz-nginx.sh (runs after aria2c thanks to zz- prefix). Serves AriaNg from /usr/local/share/ariang/ and proxies /jsonrpc + /rpc to http://127.0.0.1:6800/jsonrpc. Logs to /data/logs/nginx/.
  • Tracker auto-update: tracker.sh (already in repo at rootfs/usr/local/bin/tracker.sh, P3TERX upstream) — runs once during __run_pre_execute_checks to refresh BT trackers in aria2.conf. Network failures during this step are non-fatal (the function tees to log).
  • AriaNg client config: a small aria-ng.config.js shipped at /config/aria2/aria-ng.config.js; the __update_conf_files hook in 00-aria2c.sh copies it over the AriaNg-installed js/aria-ng-<hash>.min.js so the UI starts pre-pointed at the local RPC port.

Packages (PACK_LIST / ENV_PACKAGES)

Verified against pkgs.alpinelinux.org (community + main, edge branch).

System glue (all required by entrypoint/init.d framework or the build steps):

  • bash — entrypoint and init.d scripts are bash.
  • tini — PID 1 init (declared in Dockerfile ENTRYPOINT).
  • curl — used by tracker.sh and the framework's healthcheck.
  • wget — fallback for downloading; used by aria2c's web seed code paths in some scenarios.
  • tzdata — TZ awareness (TZ=America/New_York env var resolves correctly).
  • ca-certificates — TLS to outbound HTTPS targets and tracker URLs.
  • unzip — unpacking the prebundled AriaNg-1.3.13.zip in 05-custom.sh.
  • jq — currently used by the legacy 05-custom.sh AriaNg-version probe; kept for parity (no other init-time use, small package).
  • pwgen — password generation if user opts into RPC secret.

Service packages:

  • aria2 — the download daemon (/usr/bin/aria2c); 1.37.0-r2 in Alpine edge.
  • nginx — front HTTP server + reverse proxy (port 80).

Kept lean: no PHP, no DB, no apache.

Configs to ship in rootfs/tmp/etc/

Wipe-and-replace at build time (per template §4). All paths under rootfs/tmp/etc/. The existing repo already ships these — preserved with the surgical edits below.

  • aria2/aria2.conf — already present. Tunings (existing): dir=/data/downloads/aria2, log=/data/logs/aria2/aria2.log, input-file=/config/aria2/aria2.session + save-session=/config/aria2/aria2.session (resume-on-restart), enable-rpc=true, rpc-listen-port=REPLACE_RPC_PORT (token replaced at runtime to 6800), rpc-listen-all=true, rpc-allow-origin-all=true, rpc-secret=REPLACE_RPC_SECRET (commented out by default by __update_conf_files), disable-ipv6=true, max-concurrent-downloads=5, continue=true, max-connection-per-server=5, min-split-size=10M, split=10, enable-http-pipelining=true, file-allocation=prealloc, console-log-level=error, save-session-interval=10, follow-torrent=true, listen-port=6888, dht-listen-port=6888, bt-seed-unverified=false, bt-save-metadata=true, on-download-complete=/etc/aria2/scripts/post-hook.sh, on-download-error=/etc/aria2/scripts/post-hook.sh, bt-tracker=... (refreshed by tracker.sh at startup).
  • aria2/aria2.session — empty placeholder so the file exists on first run (aria2c errors if input-file points at a missing file when not also doing --continue).
  • aria2/aria-ng.config.js — the AriaNg client config bundle. Already present (369 KB). At runtime it's overlayed onto the AriaNg-installed js/aria-ng-*.min.js so the UI auto-connects to the local RPC.
  • aria2/scripts/post-hook.sh — already present; called by aria2c on download complete/error.
  • nginx/nginx.conf — already present (existing layout). Edits required:
    • Replace the broken daemon off; line (no daemon directive should appear in our nginx since nginx -c runs in foreground via the framework's PID supervision; safer to drop and let the framework manage it).
    • Replace REPLACE_SERVER_PORT with 80 literal (or add a token-replace hook in zz-nginx.sh::__update_conf_files_local). Currently, the existing __update_conf_files in zz-nginx.sh only replaces REPLACE_RPC_PORT and REPLACE_SERVER_ADDR — the REPLACE_SERVER_PORT is left unsubstituted, which breaks nginx -t. Fix in zz-nginx.sh: add __replace "REPLACE_SERVER_PORT" "${SERVICE_PORT:-80}" "$CONF_DIR/nginx.conf".
    • The proxy_pass line uses http://REPLACE_SERVER_ADDR:REPLACE_RPC_PORT/jsonrpc — at runtime, REPLACE_SERVER_ADDR is replaced with the container's IPv4 address (computed via __get_ip4) and REPLACE_RPC_PORT with 6800. We change REPLACE_SERVER_ADDR to 127.0.0.1 to avoid relying on container IP discovery (which can race during init); the loopback always works since aria2c binds 0.0.0.0:6800.
    • Add a final include /config/nginx/vhosts.d/*.conf; (optional include) inside the http {} block to allow user-supplied vhost overrides (per template §4 anti-pattern: must be optional).
  • nginx/mime.types — already present (preserves Alpine's mime types).

/config// layout (user-editable)

Framework's __initialize_system_etc symlinks every file under /config/<svc>/ back to its /etc/<svc>/ peer at runtime.

  • /config/aria2/aria2.conf -> /etc/aria2/aria2.conf (the running aria2c reads /config/aria2/aria2.conf directly via --conf-path from the init.d EXEC_CMD_ARGS).
  • /config/aria2/aria2.session -> /etc/aria2/aria2.session (resume-state, written by aria2c on shutdown / interval).
  • /config/aria2/aria-ng.config.js -> /etc/aria2/aria-ng.config.js (overlayed by __update_conf_files onto the AriaNg-installed JS).
  • /config/aria2/scripts/post-hook.sh -> /etc/aria2/scripts/post-hook.sh.
  • /config/nginx/nginx.conf -> /etc/nginx/nginx.conf.
  • /config/nginx/mime.types -> /etc/nginx/mime.types.
  • /config/nginx/vhosts.d/*.conf -> picked up by the optional include in nginx.conf for user overrides.
  • /config/secure/auth/{root,user}/aria2c_{name,pass} — generated by the framework if env vars set.
  • /config/env/{aria2c,nginx}.sh — per-service env overrides (RPC_PORT, RPC_SECRET, etc.).

ADDITIONAL_CONFIG_DIRS for aria2c stays empty (single config dir handled via CONF_DIR); nginx's ADDITIONAL_CONFIG_DIRS also empty (its CONF_DIR is /config/nginx).

init.d scripts

Two init.d scripts (already in repo). aria2c starts first (00- prefix), nginx starts last (zz- prefix) so the RPC backend is up when nginx starts proxying.

rootfs/usr/local/etc/docker/init.d/00-aria2c.sh — preserved as-is, already correctly configured:

  • SERVICE_NAME="aria2c", EXEC_CMD_BIN='aria2c', EXEC_CMD_ARGS='--conf-path=$CONF_DIR/aria2.conf'
  • CONF_DIR="/config/aria2", ETC_DIR="/etc/aria2", DATA_DIR="/data/aria2", LOG_DIR="/data/logs/aria2"
  • SERVICE_PORT="6800", RUNAS_USER="root"
  • IS_WEB_SERVER="no", IS_DATABASE_SERVICE="no", USES_DATABASE_SERVICE="no", DATABASE_SERVICE_TYPE="sqlite"
  • __run_pre_execute_checks: runs tracker.sh to refresh BT trackers (already wired).
  • __update_conf_files: replaces REPLACE_RPC_PORT, REPLACE_SERVER_ADDR, optionally enables rpc-secret from env, copies aria-ng.config.js over the AriaNg JS bundle. Already wired correctly.

rootfs/usr/local/etc/docker/init.d/zz-nginx.sh — preserved structure, with one fix in __update_conf_files:

  • Add __replace "REPLACE_SERVER_PORT" "${SERVICE_PORT:-80}" "$CONF_DIR/nginx.conf" so the listen directive resolves.
  • Change the __replace for REPLACE_SERVER_ADDR to use 127.0.0.1 literal instead of $CONTAINER_IP4_ADDRESS (avoids container-IP discovery races; loopback always works).
  • SERVICE_NAME="nginx", EXEC_CMD_BIN='nginx', EXEC_CMD_ARGS='-c $CONF_DIR/nginx.conf'
  • IS_WEB_SERVER="yes", SERVICE_PORT="80", WWW_ROOT_DIR="/usr/local/share/ariang"

05-custom.sh additions

Replace the network-fetching version with a host-prebundle workflow:

  1. Wipe distro-default /etc/{aria2,nginx}/* and copy in our shipped configs (preserving /etc/nginx/mime.types from tmp/etc/nginx/):
    for d in aria2 nginx; do
      [ -d "/tmp/etc/$d" ] || continue
      rm -Rf "/etc/$d"/*
      cp -Rf "/tmp/etc/$d/." "/etc/$d/"
    done
    
  2. Install AriaNg from the prebundled zip at /tmp/ariang-src/AriaNg-*.zip (placed there by 03-files.sh which copies rootfs/tmp/ariang-src/):
    ARIANG_HOME="/usr/local/share/ariang"
    rm -Rf "$ARIANG_HOME"
    mkdir -p "$ARIANG_HOME"
    ARIANG_ZIP="$(ls /tmp/ariang-src/AriaNg-*.zip 2>/dev/null | head -n1 || true)"
    if [ -n "$ARIANG_ZIP" ] && [ -f "$ARIANG_ZIP" ]; then
      unzip -qq "$ARIANG_ZIP" -d "$ARIANG_HOME"
    else
      echo "ERROR: AriaNg zip not prebundled at /tmp/ariang-src/AriaNg-*.zip" >&2
      exit 9
    fi
    
  3. Stage /usr/local/share/template-files/config/aria2/ from the live /etc/aria2/ so __initialize_config_dir seeds /config/aria2/ on first run (03-files.sh already does this — confirm it ran).
  4. Create runtime dirs: mkdir -p /run/nginx /var/log/nginx /data/downloads/aria2 /data/logs/aria2 /data/logs/nginx.

04-users.sh additions

The nginx Alpine package creates the nginx user. aria2c runs as root per RUNAS_USER="root" in 00-aria2c.sh (downloads land under /data with permissive perms). No extra users needed; leave 04-users.sh as a placeholder.

02-packages.sh additions

Empty placeholder is fine — no compile, no pip step. AriaNg is unpacked in 05-custom.sh.

Dockerfile changes

Surgical edits to the existing Dockerfile (preserve structure):

  • Update BUILD_DATE to 202605101200 (today, 2026-05-10).
  • Update PACK_LIST from "aria2 unzip nginx " to "aria2 bash tini curl wget tzdata ca-certificates unzip jq pwgen nginx " (single space separated, trailing space).
  • Change SERVICE_PORT="80" (already correct) and keep EXPOSE_PORTS="6800 6888".
  • PHP_VERSION="none" (no PHP).

.env.scripts changes

  • ENV_PACKAGES="aria2 bash tini curl wget tzdata ca-certificates unzip jq pwgen nginx" (mirrors PACK_LIST minus trailing space).
  • SERVICE_PORT="80", EXPOSE_PORTS="6800 6888".
  • PHP_VERSION="none".

.gitignore changes

Add an exception so the prebundled AriaNg zip is NOT shipped to git:

rootfs/tmp/ariang-src/*.zip

Verification (success criteria)

  1. cd /root/Projects/github/casjaysdevdocker/aria2 && rm -f .build_failed && buildx run Dockerfile succeeds for both linux/amd64 and linux/arm64. Single retry permitted on transient registry errors.
  2. docker run -d --rm --name test-aria2 -p 18080:80 -p 16800:6800 docker.io/casjaysdevdocker/aria2:latest boots; after ~30s docker logs test-aria2 | tail -50 shows aria2c "IPv4 RPC: listening on TCP port 6800" and nginx "ready" with no fatal errors.
  3. docker exec test-aria2 sh -c 'ss -tnlp 2>/dev/null || netstat -tnlp' shows ports 80 (nginx) and 6800 (aria2c) listening.
  4. curl -fsS -X POST http://localhost:16800/jsonrpc -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"q","method":"aria2.getVersion"}' -w '\nRPC:%{http_code}\n' returns 200 with a JSON body containing "version".
  5. curl -fsS -o /dev/null -w 'UI:%{http_code}\n' http://localhost:18080/ returns 200.
  6. curl -fsS -X POST http://localhost:18080/jsonrpc -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"q","method":"aria2.getVersion"}' returns the same JSON (proxied path works).
  7. docker exec test-aria2 ls /config/aria2/ /config/nginx/ /usr/local/share/ariang/index.html — every path exists.
  8. docker stop test-aria2.

Rollback

If anything goes wrong, code changes can be reverted via git checkout -- rootfs/ Dockerfile .env.scripts. Prebundled AriaNg zip is gitignored so it can't accidentally land in commits. New files (PLAN.md, CLAUDE.md if first-time) are tracked separately.

Smoke test result — fully passing

  • aria2c RPC on :6800: aria2.getVersion returns version: 1.37.0 (HTTP 200).
  • AriaNg web UI on :80: HTML loads (HTTP 200, <html ng-app="ariaNg">).
  • /config/aria2/{aria2.conf,aria2.session,aria-ng.config.js,scripts/} and /config/nginx/ seeded; AriaNg static at /usr/local/share/ariang/.
  • Container reports healthy.

Architectural notes (resolved)

  • The framework's __start_init_scripts only runs the first init.d/*.sh reliably. The original repo had 00-aria2c.sh + zz-nginx.sh and only aria2c was started. Resolution: single init.d script + wrapper:
    • rootfs/usr/local/etc/docker/init.d/00-aria2c.sh now points EXEC_CMD_BIN at /usr/local/etc/docker/bin/start-aria2.
    • rootfs/usr/local/etc/docker/bin/start-aria2 backgrounds aria2c, waits for the RPC port, then execs nginx.
    • zz-nginx.sh removed.
  • rootfs/tmp/etc/nginx/nginx.conf had unsubstituted REPLACE_SERVER_PORT/REPLACE_SERVER_ADDR/REPLACE_RPC_PORT placeholders — those were originally substituted by the now-removed zz-nginx.sh's __update_conf_files hook. Resolution: hardcoded the values (port 80, upstream 127.0.0.1:6800) since they're container-internal.

Host-side prebuild step

mkdir -p rootfs/tmp/ariang-src
curl -fsSL -o rootfs/tmp/ariang-src/AriaNg-1.3.13.zip \
  https://github.com/mayswind/AriaNg/releases/download/1.3.13/AriaNg-1.3.13.zip

(Ignored by .gitignore; 05-custom.sh extracts to /usr/local/share/ariang/ at build time.)