Skip to content

rTorrent/ruTorrent with Nginx & PHP-FPM on Entware

Note

Some assumptions:
- SSH access to device
- Entware installed
- Neither rTorrent/ruTorrent, nor Nginx nor PHP-FPM previously installed

Install required base packages

$ opkg install bash curl ffmpeg git git-http gzip htop iotop mediainfo mktorrent\
nano nginx-ssl php8 php8-cli php8-fpm php8-mod-bcmath php8-mod-ctype\
php8-mod-curl php8-mod-filter php8-mod-opcache php8-mod-phar php8-mod-session\
php8-mod-xml procps-ng-pgrep python3 python3-pip rtorrent-rpc socat unrar\
unzip wget-ssl

Configure rTorrent

Create necessary directories:

$ mkdir --parents /share/data/{.rtorrent,torrents}
$ mkdir /share/data/.rtorrent/{.session,watch}

Create rTorrent configuration file:

$ nano /share/data/.rtorrent/.rtorrent.rc
## Run rtorrent as a daemon (disables user interface, only XMLRPC control)
system.daemon.set = true

## Control rtorrent via UNIX XMLRPC socket
network.scgi.open_local = (cat, "/opt/var/run/rtorrent.sock")
execute.nothrow = chmod, 770, (cat, "/opt/var/run/rtorrent.sock")

## Listening port for incoming peer traffic, don't randomize
network.port_random.set = no
network.port_range.set = 49879-49879

## Set default data save directory, session file path
directory.default.set = /share/data/torrents
session.path.set = /share/data/.rtorrent/.session

## Disable UDP protocol to trackers, disable DHT/PEX BitTorrent protocols and enable traffic encryption if possible
trackers.use_udp.set = no
dht.mode.set = off
protocol.pex.set = no
protocol.encryption.set = allow_incoming, try_outgoing, enable_retry

## Preferred filename encoding
encoding.add = UTF-8

## Disable hash check on torrents that have finished downloading (intensive process)
pieces.hash.on_completion.set = no

## Minimum and maximum number of peers to connect to per torrent while downloading.
throttle.min_peers.normal.set = 32
throttle.max_peers.normal.set = 64

## Minimum and maximum number of peers to connect to per torrent while seeding. (-1 for same value as above)
throttle.min_peers.seed.set = -1
throttle.max_peers.seed.set = -1

## Maximum number of simultaneous downloads and uploads slots per torrent
throttle.max_downloads.set = 32
throttle.max_uploads.set = 32

## Memory address space to map file chunks.
pieces.memory.max.set = 2048M

## Watch directory, saving a torrent file to this directory will automatically start the download.
schedule2 = watch_directory, 5, 5, load.start=/share/data/.rtorrent/watch/*.torrent
schedule2 = untied_directory, 5, 5, stop_untied=/share/data/.rtorrent/watch/*.torrent

## Stop rtorrent from downloading data when disk space is low.
schedule2 = low_diskspace, 15, 60, ((close_low_diskspace, 50G))

## Save rtorrent session data every 5 minutes (default: 20min)
schedule2 = session_save, 300, 300, ((session.save))

## Initialize ruTorrent plugins
execute2 = {sh,-c,/opt/bin/php-cli /opt/var/www/rutorrent/php/initplugins.php &}

## Logging:
## levels = critical error warn notice info debug
## groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_
# Log info
# log.open_file = "logfile", (cat,/opt/var/log/rtorrent.log)
# log.add_output = "debug", "logfile"
# log.add_output = "tracker_debug", "logfile"

Create rTorrent init file:

Note

This init file is heavily inspired by Entware's /opt/etc/init.d/rc.func.

$ nano /opt/etc/init.d/S85rtorrent
#!/bin/sh

PROCS=rtorrent
ARGS="-D -n -o import=/share/data/.rtorrent/.rtorrent.rc"
DESC=$PROCS
PATH=/opt/sbin:/opt/bin:/opt/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

ansi_red="\033[1;31m";
ansi_white="\033[1;37m";
ansi_green="\033[1;32m";
ansi_yellow="\033[1;33m";
ansi_blue="\033[1;34m";
ansi_bell="\007";
ansi_blink="\033[5m";
ansi_std="\033[m";
ansi_rev="\033[7m";
ansi_ul="\033[4m";

ACTION=$1

start() {
    echo -e -n "$ansi_white Starting $DESC... $ansi_std"
    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_yellow already running. $ansi_std"
        return 0
    fi
    $PROC $ARGS > /dev/null 2>&1 &
    COUNTER=0
    LIMIT=10
    while [ -z "$(pgrep -x -f "$PROC $ARGS")" -a "$COUNTER" -le "$LIMIT" ]; do
        sleep 1;
        COUNTER=$((COUNTER + 1))
    done

    if [ -z "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_red failed. $ansi_std"
        logger "Failed to start $DESC from $CALLER."
        return 255
    else
        echo -e "            $ansi_green done. $ansi_std"
        logger "Started $DESC from $CALLER."
        return 0
    fi
}

stop() {
    case "$ACTION" in
        stop | restart)
            echo -e -n "$ansi_white Shutting down $PROC... $ansi_std"
            kill "$(pgrep -x -f "$PROC $ARGS")" 2>/dev/null
            COUNTER=0
            LIMIT=10
            while [ -n "$(pgrep -x -f "$PROC $ARGS")" -a "$COUNTER" -le "$LIMIT" ]; do
                sleep 1;
                COUNTER=$((COUNTER + 1))
            done
            rm /opt/var/run/rtorrent.sock
            ;;
        kill)
            echo -e -n "$ansi_white Killing $PROC... $ansi_std"
            kill -9 "$(pgrep -x -f "$PROC $ARGS")" 2>/dev/null
            rm /opt/var/run/rtorrent.sock
            ;;
    esac

    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "       $ansi_red failed. $ansi_std"
        return 255
    else
        echo -e "       $ansi_green done. $ansi_std"
        return 0
    fi
}

check() {
    echo -e -n "$ansi_white Checking $DESC... "
    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_green alive. $ansi_std";
        return 0
    else
        echo -e "            $ansi_red dead. $ansi_std";
        return 1
    fi
}

for PROC in $PROCS; do
    case $ACTION in
        start)
            start
            ;;
        stop | kill )
            check && stop
            ;;
        restart)
            check > /dev/null && stop
            start
            ;;
        check)
            check
            ;;
        *)
            echo -e "$ansi_white Usage: $0 (start|stop|restart|check)$ansi_std"
            exit 1
            ;;
    esac
done

Then run:

$ chmod +x /opt/etc/init.d/S85rtorrent

Install and configure ruTorrent

$ mkdir /opt/var/www
$ git clone https://github.com/Novik/ruTorrent.git /opt/var/www/rutorrent

Create ruTorrent 'user' config

We're not creating any ruTorrent users so we'll edit the default config.php file.

$ nano /opt/var/www/rutorrent/conf/config.php

Add the following:

<?php
        // configuration parameters

        // for snoopy client
        $httpUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
        $httpTimeOut = 30;                      // in seconds
        $httpUseGzip = true;
        $httpIP = null;                         // IP string. Or null for any.
        $httpProxy = array
        (
                'use'   => false,
                'proto' => 'http',              // 'http' or 'https'
                'host'  => 'PROXY_HOST_HERE',
                'port'  => 3128
        );

        // for xmlrpc actions
        $rpcTimeOut = 5;                        // in seconds
        $rpcLogCalls = false;
        $rpcLogFaults = true;

        // for php
        $phpUseGzip = false;
        $phpGzipLevel = 2;

        $schedule_rand = 10;                    // rand for schedulers start, +0..X seconds

        $do_diagnostic = true;                  // Diagnose ruTorrent. Recommended to keep enabled, unless otherwise required.
        $al_diagnostic = true;                  // Diagnose auto-loader. Set to "false" to make composer plugins work.

        $log_file = '/opt/var/log/rutorrent/errors.log';                // path to log file (comment or leave blank to disable logging)

        $saveUploadedTorrents = true;           // Save uploaded torrents to profile/torrents directory or not
        $overwriteUploadedTorrents = false;     // Overwrite existing uploaded torrents in profile/torrents directory or make unique name

        $topDirectory = '/share/data/torrents/';                        // Upper available directory. Absolute path with trail slash.
        $forbidUserSettings = false;

        // $scgi_port = 5000;
        // $scgi_host = "127.0.0.1";

        // For web->rtorrent link through unix domain socket
        // (scgi_local in rtorrent conf file), change variables
        // above to something like this:
        //

        $scgi_port = 0;
        $scgi_host = "unix:///opt/var/run/rtorrent.sock";

        $XMLRPCMountPoint = "/RPC2";            // DO NOT DELETE THIS LINE!!! DO NOT COMMENT THIS LINE!!!

        $throttleMaxSpeed = 327625*1024;        // DO NOT EDIT THIS LINE!!! DO NOT COMMENT THIS LINE!!!
        // Can't be greater then 327625*1024 due to limitation in libtorrent ResourceManager::set_max_upload_unchoked function.

        $pathToExternals = array(
                "curl"   => '/opt/bin/curl',
                "gzip"   => '/opt/bin/gzip',
                "id"     => '/usr/bin/id',
                "php"    => '/opt/bin/php-cli',
                "pgrep"  => '/opt/bin/pgrep',
                "python" => '/opt/bin/python3',
                "stat"   => '/opt/bin/stat'
        );

        $localHostedMode = true;                // Set to true if rTorrent is hosted on the SAME machine as ruTorrent

        $cachedPluginLoading = true;            // Set to true to enable rapid cached loading of ruTorrent plugins
                                                // Required to clear web browser cache when upgrading versions

        $localhosts = array(                    // list of local interfaces
                "127.0.0.1",
                "localhost",
        );

        $profilePath = '../../share';           // Path to user profiles
        $profileMask = 0777;                    // Mask for files and directory creation in user profiles.
                                                // Both Webserver and rtorrent users must have read-write access to it.
                                                // For example, if Webserver and rtorrent users are in the same group then the value may be 0770.

        $tempDirectory = null;                  // Temp directory. Absolute path with trail slash. If null, then autodetect will be used.

        $canUseXSendFile = false;               // If true then use X-Sendfile feature if it exist

        $locale = "UTF8";

        $enableCSRFCheck = false;               // If true then Origin and Referer will be checked
        $enabledOrigins = array();              // List of enabled domains for CSRF check (only hostnames, without protocols, port etc.).
                                                // If empty, then will retrieve domain from HTTP_HOST / HTTP_X_FORWARDED_HOST

Configure ruTorrent plugins follows:

$ nano /opt/var/www/rutorrent/conf/plugins.ini
;; Plugins' permissions.
;; If flag is not found in plugin section, corresponding flag from "default" section is used.
;; If flag is not found in "default" section, it is assumed to be "yes".
;;
;; For setting individual plugin permissions you must write something like that:
;;
;; [ratio]
;; enabled = yes                        ;; also may be "user-defined", in this case user can control plugin's state from UI
;; canChangeToolbar = yes
;; canChangeMenu = yes
;; canChangeOptions = no
;; canChangeTabs = yes
;; canChangeColumns = yes
;; canChangeStatusBar = yes
;; canChangeCategory = yes
;; canBeShutdowned = yes

[default]
enabled = user-defined
canChangeToolbar = yes
canChangeMenu = yes
canChangeOptions = yes
canChangeTabs = yes
canChangeColumns = yes
canChangeStatusBar = yes
canChangeCategory = yes
canBeShutdowned = yes

[ipad]
enabled = no

[httprpc]
enabled = no

[retrackers]
enabled = no

[rpc]
enabled = no

[rutracker_check]
enabled = no

[geoip]
enabled = no

[geoip2]
enabled = yes

[spectrogram]
enabled = no

Configure create plugin

$ nano /opt/var/www/rutorrent/plugins/create/conf.php
$useExternal = 'mktorrent';
$pathToCreatetorrent = '/opt/bin/mktorrent';

Install cloudscraper plugin

cloudscraper is a dependency of the _cloudflare plugin.

$ pip3 install cloudscraper

Install ratiocolor plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:Micdu70/rutorrent-ratiocolor.git ratiocolor

Install geoip2 plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:Micdu70/geoip2-rutorrent.git geoip2

Install nfo plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:phracker/ruTorrent-nfo.git nfo

Hint

You might need to "fix" tracklabels missing icons as described here.

Configure PHP/PHP-FPM

For PHP:

$ nano /opt/etc/php.ini
memory_limit = 128M
;doc_root = "/opt/share/www"
upload_max_filesize = 64M
max_file_uploads = 64
date.timezone = "Europe/Luxembourg"
sys_temp_dir = "/opt/tmp"

For PHP-FPM:

$ nano /opt/etc/php8-fpm.d/www.conf
user = admin
env[PATH] = /opt/bin:/opt/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/bin/X11:/usr/local/sbin:/usr/local/bin

Allow PHP-FPM to run as root

$ nano /opt/etc/init.d/S79php8-fpm
ARGS="--daemonize -R --fpm-config /opt/etc/php8-fpm.conf"

Note

You'll need to edit the init file to allow php-fpm to run as root each time the package is updated.

Configure Nginx

$ nano /opt/etc/nginx/nginx.conf
user admin administrators;
worker_processes auto;
pid /opt/var/run/nginx.pid;
include /opt/etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    use epoll;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
    keepalive_timeout 65;
    keepalive_requests 100;
    keepalive_disable msie6;
    types_hash_max_size 2048;
    server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /opt/etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # SSL Settings
    ##

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    ##
    # Logging Settings
    ##

    access_log /opt/var/log/nginx/access.log;
    error_log /opt/var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";
    gzip_min_length 512;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_types
        text/css
        text/javascript
        text/xml
        text/plain
        text/x-component
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        application/vnd.ms-fontobject
        font/truetype
        font/opentype
        image/svg+xml;

    ##
    # Virtual Host Configs
    ##

    include /opt/etc/nginx/conf.d/*.conf;
    include /opt/etc/nginx/sites-enabled/*;
}

Create the proper Nginx symlink

$ ln -s /opt/etc/nginx/sites-available/rutorrent.example.com.conf /opt/etc/nginx/sites-enabled/rutorrent.example.com.conf

Configure Nginx SCGI

$ nano /opt/etc/nginx/scgi_params
scgi_param  REQUEST_METHOD     $request_method;
scgi_param  REQUEST_URI        $request_uri;
scgi_param  QUERY_STRING       $query_string;
scgi_param  CONTENT_TYPE       $content_type;

scgi_param  DOCUMENT_URI       $document_uri;
scgi_param  DOCUMENT_ROOT      $document_root;
scgi_param  SCGI               1;
scgi_param  SERVER_PROTOCOL    $server_protocol;
scgi_param  REQUEST_SCHEME     $scheme;
scgi_param  HTTPS              $https if_not_empty;

scgi_param  REMOTE_ADDR        $remote_addr;
scgi_param  REMOTE_PORT        $remote_port;
scgi_param  SERVER_PORT        $server_port;
scgi_param  SERVER_NAME        $server_name;

Create Nginx vhost

$ mkdir --parent /opt/etc/nginx/{sites-available,sites-enabled}
$ nano /opt/etc/nginx/sites-available/rutorrent.example.com.conf
server {
    listen 80;
    server_name rutorrent.example.com;
    return 301 https://$server_name$request_uri;

    access_log /opt/var/log/nginx/example.com/rutorrent/access.log;
    error_log /opt/var/log/nginx/example.com/rutorrent/error.log;
}

server {
    listen 443 ssl;
    http2 on;
    server_name rutorrent.example.com;

    access_log /opt/var/log/nginx/example.com/rutorrent/access.log;
    error_log /opt/var/log/nginx/example.com/rutorrent/error.log;

    ssl_certificate /opt/etc/acme.sh/rutorrent.example.com_ecc/fullchain.cer;
    ssl_certificate_key /opt/etc/acme.sh/rutorrent.example.com_ecc/rutorrent.example.com.key;
    ssl_dhparam /opt/etc/acme.sh/rutorrent.example.com_ecc/dhparam.pem;
    ssl_session_timeout 5m;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    add_header Strict-Transport-Security "max-age=15768000" always; # six month

    charset utf-8;
    index index.html index.php;
    root /opt/var/www/rutorrent;

    client_max_body_size 64M;

    error_page 500 502 503 504 /50x.html;

    location /RPC2 {
        include scgi_params;
        scgi_pass unix:/opt/var/run/rtorrent.sock;
    }

    location = /50x.html {
        root /opt/share/nginx/html;
    }

    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    location ~ ^/(conf|share)/(.+)$ {
        deny all;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_index  index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS on;
        # Avoid sending the security headers twice
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass unix:/opt/var/run/php8-fpm.sock;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
    }

    location ~* \.(jpg|jpeg|gif|css|png|js|map|woff|woff2|ttf|svg|eot)$ {
        expires 30d;
        access_log off;
    }
}

Generate TLS certificates for vhost

Install acme.sh:

$ cd /tmp
$ git clone https://github.com/acmesh-official/acme.sh.git
$ cd acme.sh/
$ ./acme.sh --install --nocron --home /opt/etc/acme.sh --accountemail "[email protected]"

Configure your DNS provider API keys:

$ nano /opt/etc/acme.sh/dnsapi/dns_cf.sh
#!/opt/bin/bash

#
#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
#
#CF_Email="[email protected]"

CF_Token="foo"
CF_Account_ID="bar"
CF_Zone_ID="foobar"

CF_Api="https://api.cloudflare.com/client/v4"

Generate TLS certificate:

$ /opt/etc/acme.sh/acme.sh --home /opt/etc/acme.sh --server letsencrypt --issue -d example.com --dns dns_cf

Setup a cronjob to automatically renew your certificate:

$ crontab -e
30 3 * * 0 "/opt/etc/acme.sh/acme.sh" --cron --home "/opt/etc/acme.sh" > /dev/null

Setup a Diffie-Helmann key as follows:

$ wget https://ssl-config.mozilla.org/ffdhe4096.txt -O /opt/etc/acme.sh/rutorrent.example.com_ecc/dhparam.pem

Restart services

$ /opt/etc/init.d/S79php8-fpm restart
$ /opt/etc/init.d/S80nginx restart
$ /opt/etc/init.d/S85rtorrent start

Hint

Nginx might error out with the following message:

nginx: [error] open() "/opt/var/run/nginx.pid" failed (2: No such file or directory)

To fix it, simply run the following command:

$ nginx -c /opt/etc/nginx/nginx.conf

You should now be all set! 🎉