Sonarr and Radarr behind a reverse proxy
Run Sonarr and Radarr behind nginx or Traefik with forward auth: the Allowed Hosts entries that stop Invalid Hostname errors, and why /api must skip login.
Sonarr and Radarr behind a reverse proxy: the short answer
Putting Sonarr and Radarr behind a reverse proxy comes down to four decisions. First, choose a subdomain or a URL Base, and make the proxy and the app agree on it. Second, put every hostname the proxy sends into Allowed Hosts. Third, hand the login to a forward-auth layer. Fourth, switch the app to External authentication and leave /api open to API-key traffic. Prowlarr follows the same rules, so everything below applies to it as well.
A reverse proxy is the web server that faces the internet and passes each request to an app behind it. Here that is nginx or Traefik. A forward-auth layer is a login service that the proxy asks before it passes a request on. oauth2-proxy and Authelia are two common choices.
Which versions this guide was checked against
As of October 2026, the current stable releases are Sonarr 4.0.20.3014, Radarr 6.4.4.10685 and Prowlarr 2.6.5.5623. All of them were released on 16 September 2026. These settings have moved between versions. Allowed Hosts and Trusted Networks first appeared in pre-releases in late August 2026, and these are the first stable builds that have them. An older build has neither field. Open System > Status and check your version before you follow the steps.
The External authentication method is older than these builds, but the settings page still hides it. A later section shows how to set it.
URL Base or subdomain: which should you use?
There are two ways to give Sonarr a public address. A subdomain gives each app its own name, such as sonarr.example.com. A URL Base puts the app under a path on a shared name, such as example.com/sonarr.
Use a subdomain when you can. The app runs at the root path, which is what it expects, so URL Base stays empty and nothing needs rewriting. Each app also gets its own proxy block, which keeps the API exemption simple. The cost is one DNS (domain name system) record and one TLS (transport layer security) certificate per app, or one wildcard certificate.
Use a URL Base when you only have one hostname. You then set the same path in two places. One is in the app, under Settings > General > URL Base. The other is the proxy's location block or path rule. The two must match. Sonarr trims slashes from the value and adds one leading slash, so sonarr, /sonarr and /sonarr/ are all stored as /sonarr. Restart the app after you change it.
How a URL Base breaks: the redirect loop
This happens when Sonarr has URL Base set to /sonarr but the proxy strips the prefix before it passes the request on. In nginx, a proxy_pass that ends in a slash does this. Inside location /sonarr/, the line proxy_pass http://127.0.0.1:8989/; turns /sonarr/ into /.
Sonarr then receives a request for /. Any request outside its URL Base gets a 307 redirect to the same path under the URL Base. The browser follows the redirect to /sonarr/. The proxy strips the prefix again, and the cycle repeats until the browser gives up with "redirected you too many times". You can watch one step of the loop on the server:
curl -sI http://127.0.0.1:8989/With URL Base set, the answer is a 307 with Location: /sonarr/. That is correct behaviour from the app. The fix is in the proxy: remove the path from proxy_pass, so nginx passes the full path through unchanged.
How a URL Base breaks: the blank page
This is the opposite mistake. The proxy serves the app under /sonarr/, but URL Base in the app is empty. Sonarr serves its main page for any path it does not know, because the web interface handles its own routing. So /sonarr/ still returns a page. But Sonarr builds the script and style links in that page from its own URL Base. With an empty URL Base, the links point at the root of the domain. The proxy does not send those paths to Sonarr, so the files never load and the page stays blank. Check the links on the server:
curl -s http://127.0.0.1:8989/sonarr/ | grep -o 'src="[^"]*"' | head -n 3Each src should start with /sonarr/. If it starts with a bare /, set URL Base in the app and restart it.
Which hostnames belong in Allowed Hosts behind a proxy?
Every HTTP request carries a Host header. It holds the name the client used, such as sonarr.example.com. Allowed Hosts is the list of names the app will answer for. When the field is empty, the app answers for any name and shows a health warning. The Allowed Hosts not configured warning has its own guide. This section covers what changes when a proxy sits in front.
The rule is simple. The app checks the name the proxy sends, not the address in proxy_pass. Nginx with proxy_set_header Host $host; sends the public name the browser used. Traefik sends the public name by default. So the list is:
- The public name people type:
sonarr.example.comfor a subdomain, orexample.comfor a URL Base. - Every container name that other apps use to reach this one. If Prowlarr syncs to
http://sonarr:8989, Sonarr seesHost: sonarr, so addsonarr. - Any IP address you still type into a browser directly, for example a private address on your VPN.
- Nothing extra for a proxy on the same machine. The app always accepts
localhost,127.0.0.1,[::1]and its own machine name, even when they are not listed.
That last point has a catch in Docker. A container's machine name is its hostname, which is a random ID unless you set one. So the service name is not added for you. You must list it.
The field has a few format rules, because of how the app parses it:
- Separate entries with commas. Semicolons also work.
- Leave out ports and schemes. The match ignores the port, and
https://sonarr.example.comis not a valid hostname. *.example.commatches any subdomain, but notexample.comitself.- CIDR ranges such as
10.0.0.0/8are not accepted here.
A typical Sonarr entry for a subdomain setup with Prowlarr in Docker is sonarr.example.com, sonarr. Save it, then restart the app, because the field only takes effect at startup. Radarr and Prowlarr have the same field, with the same rules.
What does Sonarr return when the Host header is not on the list?
It returns HTTP 400 with a short HTML page. Sonarr, Radarr and Prowlarr run on ASP.NET Core, the Microsoft web framework, and Allowed Hosts feeds that framework's host filtering. The filter writes the response itself. You can see it on the server, without the proxy:
curl -si -H 'Host: not-listed.example' http://127.0.0.1:8989/pingThe first line is HTTP/1.1 400 Bad Request. The body contains the two lines that people search for:
Bad Request - Invalid Hostname
HTTP Error 400. The request hostname is invalid.Send a name that is on the list, or drop the -H option, and the same request returns 200. The check only runs once Allowed Hosts has at least one entry. An empty field accepts every name.
The filter runs before authentication. A correct API key does not help, so a Prowlarr sync to an unlisted container name fails with 400 even when the key is right. You can prove this from inside the Prowlarr container:
docker compose exec prowlarr curl -si http://sonarr:8989/ping | head -n 1HTTP/1.1 400 Bad Request means sonarr is missing from Sonarr's Allowed Hosts.
The filter also runs before Sonarr's own request logging, so the normal request lines never show these rejections. Since 4.0.20, the filter's own messages are written to the log at Info level, and that is where the rejected name appears.
If a 400 page has exactly this text, the request reached the app and the app refused the name. Nginx and Traefik do not produce this page. Fix Allowed Hosts, or fix the Host header the proxy sends.
Nginx: a subdomain with forward auth and an open /api
This server block puts Sonarr at sonarr.example.com and sends every browser request through oauth2-proxy. API-key traffic goes straight to the app. The oauth2-proxy blocks follow the nginx example in the oauth2-proxy documentation. Setting up oauth2-proxy itself is covered in protecting self-hosted apps with oauth2-proxy.
server {
listen 443 ssl;
server_name sonarr.example.com;
ssl_certificate /etc/letsencrypt/live/sonarr.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sonarr.example.com/privkey.pem;
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location @oauth2_signin {
return 302 /oauth2/sign_in?rd=$scheme://$host$request_uri;
}
# API-key traffic from Prowlarr and mobile apps. No auth_request here.
location ^~ /api/ {
proxy_pass http://127.0.0.1:8989;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
proxy_pass http://127.0.0.1:8989;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
}A few lines matter more than the rest. location ^~ /api/ is a longer prefix than location /, so nginx picks it for every API path, and it has no auth_request line. proxy_set_header Host $host; is why the public name goes into Allowed Hosts. X-Forwarded-Host $host is set on purpose. Nginx passes client headers through unchanged, and Sonarr trusts forwarded headers that arrive from the loopback address. When you set the header yourself, the name Sonarr checks always comes from nginx and never from the client. The Upgrade and Connection lines let the browser open the WebSocket connection that the interface uses for live updates.
Check the syntax, then reload:
sudo nginx -t
sudo systemctl reload nginxnginx -t should end with test is successful. For the certificate paths above, see getting a Let's Encrypt certificate for nginx with certbot. For Radarr, copy the block and change the name and the port to 7878. Prowlarr uses port 9696.
Nginx: the same app under a URL Base
On a shared hostname, the app lives under its path, and the API exemption moves with it. Keep the oauth2-proxy blocks from the example above in the same server block, then add:
location ^~ /sonarr/api/ {
proxy_pass http://127.0.0.1:8989;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /sonarr {
auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
proxy_pass http://127.0.0.1:8989;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}Both proxy_pass lines end at the port, with no path and no trailing slash. That keeps /sonarr in the request, so it matches the URL Base set in the app. Here Allowed Hosts lists example.com, because that is the name in the Host header. For the reasoning behind each directive, read the nginx reverse proxy config, explained line by line.
Traefik: labels for the app and its API router
With Traefik, the same split is two routers on one service. One router covers the whole hostname and carries the forward-auth middleware. The second router matches only /api and has no middleware. This example uses the forward-auth address from the Authelia documentation. The authelia middleware is defined on the Authelia container.
services:
authelia:
# image, volumes and configuration omitted
labels:
- traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth
- traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true
- traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email,Remote-Name
sonarr:
image: lscr.io/linuxserver/sonarr:latest
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- SONARR__SERVER__ALLOWEDHOSTS=sonarr.example.com,sonarr
labels:
- traefik.enable=true
- traefik.http.services.sonarr.loadbalancer.server.port=8989
- traefik.http.routers.sonarr.rule=Host(`sonarr.example.com`)
- traefik.http.routers.sonarr.entrypoints=websecure
- traefik.http.routers.sonarr.tls=true
- traefik.http.routers.sonarr.service=sonarr
- traefik.http.routers.sonarr.middlewares=authelia@docker
- traefik.http.routers.sonarr-api.rule=Host(`sonarr.example.com`) && PathPrefix(`/api`)
- traefik.http.routers.sonarr-api.entrypoints=websecure
- traefik.http.routers.sonarr-api.tls=true
- traefik.http.routers.sonarr-api.service=sonarr
- traefik.http.routers.sonarr-api.priority=100Traefik gives each router a default priority equal to the length of its rule, so the longer API rule already wins. The explicit priority=100 keeps it that way if you shorten the rules later. The Sonarr service has no ports: section. Traefik reaches it over the Docker network, so nothing outside can reach port 8989 directly. That matters in the next section. If you use authentik instead of Authelia, the middleware address changes and the router split stays the same. See authentik forward auth with Traefik, or Authelia vs authentik if you have not picked one yet.
Set Authentication to External only once the proxy enforces login
External means the app does no login check of its own. In the Sonarr source, the External method uses the same handler as None. Whoever reaches the app's port gets in. So switch only after two things are true.
First, the proxy must enforce login. From your own computer:
curl -s -o /dev/null -w '%{http_code}\n' https://sonarr.example.com/You want 302 (a redirect to the login page) or 401. A 200 means the proxy is passing requests straight through, so stop and fix the proxy first.
Second, the app's own port must be closed to the internet. Run this from your own computer, not the server:
curl -m 5 http://203.0.113.10:8989/pingReplace the address with your VPS address. The command should time out or report Connection refused. Any response means anyone can skip the proxy. With Docker, publish the port as 127.0.0.1:8989:8989, or not at all. A plain 8989:8989 is published on every interface, and Docker's firewall rules sit ahead of ufw, so ufw does not block it. Which arr stack ports to expose, and which to keep private covers the rest of the stack.
Why not keep the app's own login as well? You can, but you will log in twice. The "Disabled for Local Addresses" option does not avoid that behind a proxy. Sonarr never treats a request that carries an X-Forwarded-For header as local, and every proxied request carries one.
The settings page does not offer External. The Authentication menu lets you pick Basic or Forms. None is shown greyed out, and External is hidden. Set it in the config file instead. For a native install, stop the app first, because it reads the file at startup and rewrites it when settings are saved:
sudo systemctl stop sonarr
sudo grep -E 'AuthenticationMethod|AllowedHosts' /var/lib/sonarr/config.xml
sudo sed -i 's|<AuthenticationMethod>[^<]*</AuthenticationMethod>|<AuthenticationMethod>External</AuthenticationMethod>|' /var/lib/sonarr/config.xml
sudo systemctl start sonarrIf grep prints no AuthenticationMethod line, sed changes nothing. Add <AuthenticationMethod>External</AuthenticationMethod> on its own line just above </Config>. Radarr keeps its file in /var/lib/radarr/config.xml, and Prowlarr in /var/lib/prowlarr/config.xml.
In Docker, use environment variables instead:
environment:
- SONARR__AUTH__METHOD=External
- SONARR__SERVER__ALLOWEDHOSTS=sonarr.example.com,sonarrRadarr uses the RADARR__ prefix and Prowlarr uses PROWLARR__. An environment variable wins over the settings page. If you later change the same setting in the page, the change has no effect until you remove the variable.
Now prove what External does. On the server:
curl -s http://127.0.0.1:8989/initialize.jsonIt returns JSON, and that JSON includes apiKey. This file is how the web interface learns the API key. With External set, anyone who reaches the port can read it. That is why the port check above comes first. The trade-offs of turning the built-in login off are covered in disabling Sonarr and Radarr authentication safely.
Why /api has to skip the forward-auth layer
Prowlarr and mobile apps talk to the API, not to the web pages, and so do download-client scripts. They prove who they are with the API key, in an X-Api-Key header or an apikey query parameter. The app checks that key on every API path, whatever the authentication method. The forward-auth layer knows nothing about API keys. It sees a request with no login cookie and answers with a redirect to an HTML login page. The client expected JSON, so the sync or the app fails.
Test both cases through the proxy:
curl -s -o /dev/null -w '%{http_code}\n' https://sonarr.example.com/api/v3/system/status
curl -s -o /dev/null -w '%{http_code}\n' -H 'X-Api-Key: YOUR_API_KEY' https://sonarr.example.com/api/v3/system/statusWith the exemption in place, the first returns 401 and the second returns 200. The 401 comes from Sonarr, because the request has no key. If both return 302, the forward-auth layer is still catching /api. Radarr's API lives under /api/v3 as well, and Prowlarr's under /api/v1.
Exempt /api and nothing wider. /initialize.json sits outside /api and holds the API key, so it must stay behind the login. A broad rule such as "skip auth for anything with an apikey parameter" is also easy to abuse, because the client controls the query string.
Prowlarr needs one more check. Apps that Prowlarr syncs read indexer feeds at paths such as /1/api and /1/download, which are outside /api/. When Sonarr and Radarr reach Prowlarr by its container name, as in http://prowlarr:9696, this traffic never touches the proxy and needs no rule. Keep it that way if you can. Connecting Prowlarr to Sonarr, Radarr and a download client shows that layout. If an app must use Prowlarr's public name, add this block to Prowlarr's server block, with no auth_request:
location ~ ^/[0-9]+/(api|download)$ {
proxy_pass http://127.0.0.1:9696;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Trusted Networks: real client addresses in the logs
By default, Sonarr trusts X-Forwarded-For and the other forwarded headers only from the loopback range 127.0.0.0/8. Nginx on the same host connects from there, so this works with no change. A proxy in a Docker container connects from a Docker network address instead. Sonarr then ignores the forwarded headers and logs the proxy's address as the client.
To fix that, find the proxy network's range and add it under Settings > General > Trusted Networks, or set SONARR__SERVER__TRUSTEDNETWORKS. Unlike Allowed Hosts, this field does accept CIDR ranges.
docker network inspect proxy | grep SubnetReplace proxy with your network's name. The output shows a line such as "Subnet": "172.18.0.0/16". Only add networks where every sender is a proxy that overwrites these headers, and restart the app after you save. If you are still planning the containers themselves, the full arr stack in Docker Compose puts them on a shared network from the start.
Check the whole chain
Run these in order after any change. Each one tests one layer.
curl -si -H 'Host: sonarr.example.com' http://127.0.0.1:8989/pingon the server returns200. Allowed Hosts accepts the public name.curl -s -o /dev/null -w '%{http_code}\n' https://sonarr.example.com/from outside returns302or401. The proxy enforces login.- The API test above returns
401without a key and200with one. The/apiexemption works and the key still matters. curl -m 5 http://203.0.113.10:8989/pingfrom outside fails. Nobody can skip the proxy.
Then open System > Status in the browser. The page should load after one login, and the health section should not list the Allowed Hosts warning.
FAQ
Why does Sonarr show "Bad Request - Invalid Hostname"?
The app has Allowed Hosts set, and the Host header of the request is not on the list. Sonarr returns HTTP 400 with "HTTP Error 400. The request hostname is invalid." before any login check. Behind a proxy, the name it checks is the one the proxy sends, usually the public name such as sonarr.example.com. Container names that other apps use, such as sonarr, must be listed too. Add the missing name in Settings > General > Allowed Hosts, separated by commas, then restart the app.
Should I use a URL Base or a subdomain for Sonarr and Radarr?
Use a subdomain if you can add DNS records and certificates, because the app then runs at the root path and URL Base stays empty. Use a URL Base when you only have one hostname. Then set the same path in the app and in the proxy, and make sure the proxy does not strip it. A stripped prefix causes a redirect loop. A missing URL Base in the app causes a blank page.
Why does Prowlarr stop syncing after I add forward auth?
The forward-auth layer answers API requests with a redirect to its login page, because it does not know about API keys. Exempt /api/ from forward auth so the app's own API key check handles those requests. A better option is to let Prowlarr and the apps reach each other by container name, such as http://sonarr:8989, so their traffic never passes through the proxy. In that case, add the container name to each app's Allowed Hosts.
Is it safe to set Sonarr authentication to External?
It is safe only when the proxy enforces login and the app's own port cannot be reached from outside. External turns off the app's login completely. Anyone who reaches the port can open the interface and read the API key from /initialize.json. Publish the port on 127.0.0.1 only, or not at all, and check from another machine that a request to port 8989 fails.
Where is the External option in Sonarr v4?
The settings page hides it. The Authentication menu lets you pick only Basic or Forms. Set <AuthenticationMethod>External</AuthenticationMethod> in config.xml while the app is stopped, or set the environment variable SONARR__AUTH__METHOD=External in Docker. Radarr and Prowlarr use the same setting with their own prefixes, RADARR__ and PROWLARR__.