Jump to content

Cleanuparr on PMSS

From Pulsed Media Wiki

Cleanuparr keeps the download queues of Sonarr, Radarr and similar apps moving: its project describes it as removing stalled, failed and malicious downloads from those queues automatically. On a Pulsed Media seedbox it runs at your own address behind its own login, in about 236 MiB of memory when idle, and the steps below create that login before any port opens. It runs in rootless Docker inside your account. Tested on a Pulsed Media seedbox on 24 October 2026 with ghcr.io/cleanuparr/cleanuparr 2.10.12: the install, the login and Cleanuparr's API through the panel address were tested; connecting it to Sonarr, Radarr or a download client was not, so its cleaning was not seen working.

Privacy

Level: LOW. By default it fetches a small status file from the project (cleanuparr-status.pages.dev) about every 10 minutes to check for new versions; Configure shows how to switch that off. Its Malware Blocker, when it is enabled, fetches community pattern lists from cleanuparr.pages.dev. Otherwise it talks to the apps, download clients and notifiers you connect, and it stores their API keys and logins. Source: Cleanuparr 2.10.12 code (AppStatusRefreshService.cs, GeneralConfig.cs, BlocklistProvider.cs in the Cleanuparr repository).

Requirements

Need Cleanuparr on PMSS
Memory About 236 MiB idle in the test, with nothing connected. userShowLimits shows your account's memory use against its limit.
Disk The image is 732 MB.
Own login Yes. The account is created in step 2, while no port is published.
Address https://SERVER/user-USERNAME/cleanuparr/ through your own web server. Cleanuparr's BASE_PATH setting carries the path, so the proxy fragment needs no map-urlpath.
Web server lighttpd 1.4.46 or newer for the fragment in step 4; /usr/sbin/lighttpd -v shows yours. The test server had 1.4.69.
Other apps Sonarr, Radarr or a similar app, and a download client; see Integrate.

Install

Run these over SSH as your own account. Replace USERNAME with your account name, SERVER with the server name in your panel address, and HOSTPORT with a free port between 2000 and 38000 that ss -tln does not list. This page calls it HOSTPORT because the settings file has its own line PORT=11011, the port inside the container; leave that line as it is. Until its account exists, Cleanuparr lets anyone who reaches it create one, and other accounts on the same server can connect to 127.0.0.1. So it runs first with no published port, gets its account, and only then gets its port.

1. Make the folder, the settings file and a random password, and start Cleanuparr with no published port. umask 077 makes the files you create in this shell readable only by you. The image is pinned to the digest that was tested.

mkdir -p ~/docker/cleanuparr/config && chmod 700 ~/docker/cleanuparr
umask 077
printf 'PUID=0\nPGID=0\nTZ=Etc/UTC\nPORT=11011\nBASE_PATH=/user-USERNAME/cleanuparr\n' > ~/docker/cleanuparr/cleanuparr.env
openssl rand -base64 24 > ~/docker/cleanuparr/admin.pw
IMG=ghcr.io/cleanuparr/cleanuparr@sha256:d0212e4c66bd0522e23c5e5da5d3006bd5ff0e0de1e3ecfcf892e039d55721e2
docker run -d --name cleanuparr --env-file ~/docker/cleanuparr/cleanuparr.env -v $HOME/docker/cleanuparr/config:/config --restart unless-stopped $IMG
sleep 30

PUID=0 and PGID=0 make Cleanuparr run as your own account; with other values its start script tries to change file owners, which rootless Docker does not allow.

2. Create the account from inside the container, in the same shell as step 1.

B=http://127.0.0.1:11011/user-USERNAME/cleanuparr
printf '{"username":"admin","password":"%s"}' "$(cat ~/docker/cleanuparr/admin.pw)" | docker exec -i cleanuparr curl -s -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/json' -d @- $B/api/auth/setup/account
docker exec cleanuparr curl -s -o /dev/null -w '%{http_code}\n' -X POST $B/api/auth/setup/complete
docker exec cleanuparr curl -s $B/api/auth/status

The first two commands print 201 and 200, as in the test, and after that a new account request was refused with Setup already completed. The last command shows Cleanuparr's setup state; before setup it showed "setupCompleted":false. Do not go on to step 3 until it shows "setupCompleted":true, because until then anyone who reaches the port can create the account. That last check was added for this guide; the test did not record its output after setup. Two-factor sign-in, which the web setup offers, is optional and was not set up.

3. Start Cleanuparr again with its port on 127.0.0.1. The account and settings stay in the config folder. The command uses IMG from step 1; in a new shell, run the IMG= line from step 1 first.

docker rm -f cleanuparr
docker run -d --name cleanuparr --env-file ~/docker/cleanuparr/cleanuparr.env -p 127.0.0.1:HOSTPORT:11011 -v $HOME/docker/cleanuparr/config:/config --restart unless-stopped $IMG

4. Add the proxy fragment and check the syntax. Run the check from your home folder.

cat > ~/.lighttpd/custom.d/cleanuparr.conf << 'EOF'
$HTTP["url"] =~ "^/user-USERNAME/cleanuparr/" {
  auth.require = ()
  $REQUEST_HEADER["Authorization"] =~ "^Basic " {
    setenv.set-request-header = ( "Authorization" => "" )
  }
  proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => HOSTPORT ) ) ),
  proxy.header = ( "upgrade" => "enable" )
}
EOF
/usr/sbin/lighttpd -t -f ~/.lighttpd.conf

The last command prints Syntax OK. If it prints an error, fix or delete cleanuparr.conf at once: a broken fragment keeps your web panel down at the next restart.

This fragment differs from most guides on this wiki. A browser logged into your panel sends your panel login, which is your account password, with every request in an Authorization: Basic header. Cleanuparr signs you in with an Authorization: Bearer token in the same header. The usual line, setenv.set-request-header = ( "Authorization" => "" ) on its own, would remove both: measured with Ombi, which uses the same kind of token, sign-in then succeeded and every request after it was refused. The condition removes only the panel's Basic login, so Cleanuparr's token gets through and your password does not. In the test, sign-in through the panel address and the request made with its token after it both succeeded with this form. auth.require = () lets requests past your panel login to Cleanuparr's own sign-in. Your web server usually picks up the file within about a minute (Lighttpd Custom Configuration); the first fragment of an account can take longer, see Troubleshooting.

What the test ran: these steps with a fixed port in place of HOSTPORT, except that the test printed the setup replies instead of only their status codes and did not run the setup-state check.

Configure

  • Open https://SERVER/user-USERNAME/cleanuparr/ and sign in as admin with the password that cat ~/docker/cleanuparr/admin.pw shows.
  • Keep Disable Auth for Local Addresses off (the default). Behind your web server every request reaches Cleanuparr from 127.0.0.1, so turning it on would remove the login for everyone on the internet.
  • To stop the version check, switch off Settings → General → Status Check. In the test the same switch, set through Cleanuparr's API, was saved and read back as off.
  • Cleanuparr removes downloads by design. Read each cleaning rule before you turn it on.

Integrate

Not tested. Cleanuparr needs the address and API key of Sonarr, Radarr or a similar app, and of your download client. Install Media Stack installs Sonarr and Radarr directly in your account. Inside a container, 127.0.0.1 is the container itself, not the server, and no address that a container can use to reach those apps has been tested on Pulsed Media servers.

Test

  1. docker ps --filter name=cleanuparr shows the container as Up.
  2. https://SERVER/user-USERNAME/cleanuparr/ leads to Cleanuparr's sign-in page. Sign in as admin. In the test the sign-in through this address returned a token, and a request made with that token after it succeeded; this was checked with Cleanuparr's API, not in a browser session.
  3. A request with no login straight to the port is refused, which is what another account on the server sees:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:HOSTPORT/user-USERNAME/cleanuparr/api/configuration/general

In the test this returned 401, and so did a sign-in with a wrong password.

Cleanuparr's sign-in page at /user-USERNAME/cleanuparr/ in the 24 October 2026 test.

Uninstall

docker rm -f cleanuparr
rm -f ~/.lighttpd/custom.d/cleanuparr.conf
touch ~/.lighttpd/custom

Deleting a fragment changes no remaining file, so touch ~/.lighttpd/custom makes your web server restart without it (deleting or disabling a fragment explains why). docker rmi ghcr.io/cleanuparr/cleanuparr@sha256:d0212e4c66bd0522e23c5e5da5d3006bd5ff0e0de1e3ecfcf892e039d55721e2 frees the image's 732 MB.

Deleting the data is your choice. rm -rf ~/docker/cleanuparr deletes the password file, the settings file and Cleanuparr's config folder with its account and the API keys and logins you entered. The test ran these commands and also removed the image.

Troubleshooting

  • A sign-in attempt answers "Account is locked": a wrong password was tried a moment before. Wait a few seconds and try again; in the test the lock lasted about a second.
  • Sign-in succeeds, then Cleanuparr's requests are refused: the fragment removes Cleanuparr's own token as well as the panel login. Use the conditional form from step 4.
  • /usr/sbin/lighttpd -t reports an error on the $REQUEST_HEADER line: your lighttpd is older than 1.4.46. Check with /usr/sbin/lighttpd -v. Not seen in the test.
  • Step 2 prints nothing or a connection error: Cleanuparr was not ready yet. Wait a little and run step 2 again.
  • The address still answers 404 several minutes after you added the fragment: your web server has not picked it up yet; in the test the first fragment of an account took about 15 minutes. Run touch ~/.lighttpd/custom.
  • docker pull or docker run fails before Cleanuparr starts: see the troubleshooting section of the Rootless DOCKER page.

See also