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
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:
aria2Alpine package ->/usr/bin/aria2c. Started by00-aria2c.shwith--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/AriaNgGitHub release) — pure static HTML/JS/CSS, no backend. Pre-bundled in repo atrootfs/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:
nginxAlpine package ->/usr/sbin/nginx. Started byzz-nginx.sh(runs after aria2c thanks tozz-prefix). Serves AriaNg from/usr/local/share/ariang/and proxies/jsonrpc+/rpctohttp://127.0.0.1:6800/jsonrpc. Logs to/data/logs/nginx/. - Tracker auto-update:
tracker.sh(already in repo atrootfs/usr/local/bin/tracker.sh, P3TERX upstream) — runs once during__run_pre_execute_checksto refresh BT trackers inaria2.conf. Network failures during this step are non-fatal (the function tees to log). - AriaNg client config: a small
aria-ng.config.jsshipped at/config/aria2/aria-ng.config.js; the__update_conf_fileshook in00-aria2c.shcopies it over the AriaNg-installedjs/aria-ng-<hash>.min.jsso 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 bytracker.shand 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_Yorkenv var resolves correctly).ca-certificates— TLS to outbound HTTPS targets and tracker URLs.unzip— unpacking the prebundledAriaNg-1.3.13.zipin05-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 to6800),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 bytracker.shat startup).aria2/aria2.session— empty placeholder so the file exists on first run (aria2c errors ifinput-filepoints 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-installedjs/aria-ng-*.min.jsso 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 (nodaemondirective should appear in our nginx sincenginx -cruns in foreground via the framework's PID supervision; safer to drop and let the framework manage it). - Replace
REPLACE_SERVER_PORTwith80literal (or add a token-replace hook inzz-nginx.sh::__update_conf_files_local). Currently, the existing__update_conf_filesinzz-nginx.shonly replacesREPLACE_RPC_PORTandREPLACE_SERVER_ADDR— theREPLACE_SERVER_PORTis left unsubstituted, which breaksnginx -t. Fix inzz-nginx.sh: add__replace "REPLACE_SERVER_PORT" "${SERVICE_PORT:-80}" "$CONF_DIR/nginx.conf". - The
proxy_passline useshttp://REPLACE_SERVER_ADDR:REPLACE_RPC_PORT/jsonrpc— at runtime,REPLACE_SERVER_ADDRis replaced with the container's IPv4 address (computed via__get_ip4) andREPLACE_RPC_PORTwith6800. We changeREPLACE_SERVER_ADDRto127.0.0.1to avoid relying on container IP discovery (which can race during init); the loopback always works since aria2c binds0.0.0.0:6800. - Add a final
include /config/nginx/vhosts.d/*.conf;(optional include) inside thehttp {}block to allow user-supplied vhost overrides (per template §4 anti-pattern: must be optional).
- Replace the broken
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.confdirectly via--conf-pathfrom 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_filesonto 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: runstracker.shto refresh BT trackers (already wired).__update_conf_files: replacesREPLACE_RPC_PORT,REPLACE_SERVER_ADDR, optionally enablesrpc-secretfrom env, copiesaria-ng.config.jsover 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 thelistendirective resolves. - Change the
__replaceforREPLACE_SERVER_ADDRto use127.0.0.1literal 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:
- Wipe distro-default
/etc/{aria2,nginx}/*and copy in our shipped configs (preserving/etc/nginx/mime.typesfromtmp/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 - Install AriaNg from the prebundled zip at
/tmp/ariang-src/AriaNg-*.zip(placed there by 03-files.sh which copiesrootfs/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 - Stage
/usr/local/share/template-files/config/aria2/from the live/etc/aria2/so__initialize_config_dirseeds/config/aria2/on first run (03-files.sh already does this — confirm it ran). - 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_DATEto202605101200(today, 2026-05-10). - Update
PACK_LISTfrom"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 keepEXPOSE_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)
cd /root/Projects/github/casjaysdevdocker/aria2 && rm -f .build_failed && buildx run Dockerfilesucceeds for bothlinux/amd64andlinux/arm64. Single retry permitted on transient registry errors.docker run -d --rm --name test-aria2 -p 18080:80 -p 16800:6800 docker.io/casjaysdevdocker/aria2:latestboots; after ~30sdocker logs test-aria2 | tail -50shows aria2c "IPv4 RPC: listening on TCP port 6800" and nginx "ready" with no fatal errors.docker exec test-aria2 sh -c 'ss -tnlp 2>/dev/null || netstat -tnlp'shows ports80(nginx) and6800(aria2c) listening.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'returns200with a JSON body containing"version".curl -fsS -o /dev/null -w 'UI:%{http_code}\n' http://localhost:18080/returns200.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).docker exec test-aria2 ls /config/aria2/ /config/nginx/ /usr/local/share/ariang/index.html— every path exists.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.getVersionreturnsversion: 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_scriptsonly runs the firstinit.d/*.shreliably. The original repo had00-aria2c.sh+zz-nginx.shand only aria2c was started. Resolution: single init.d script + wrapper:rootfs/usr/local/etc/docker/init.d/00-aria2c.shnow pointsEXEC_CMD_BINat/usr/local/etc/docker/bin/start-aria2.rootfs/usr/local/etc/docker/bin/start-aria2backgroundsaria2c, waits for the RPC port, thenexecsnginx.zz-nginx.shremoved.
rootfs/tmp/etc/nginx/nginx.confhad unsubstitutedREPLACE_SERVER_PORT/REPLACE_SERVER_ADDR/REPLACE_RPC_PORTplaceholders — those were originally substituted by the now-removedzz-nginx.sh's__update_conf_fileshook. 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.)