Guide
Why does Nextcloud say "Access through untrusted domain" behind a reverse proxy?
Because the hostname your proxy passes on is not in `trusted_domains`, and editing the compose file will not put it there. We installed Nextcloud 35 in Docker, changed every proxy variable after the fact, and put it behind Caddy and Traefik to see which settings actually take effect.
Why does Nextcloud say Access through untrusted domain behind a reverse proxy?
The Host header the proxy forwards, usually your public hostname, is not in trusted_domains in config.php. In the official Docker image, NEXTCLOUD_TRUSTED_DOMAINS is applied only once, during the automated install, so changing it in the compose file later does nothing. Add the name with docker compose exec -u www-data app php occ config:system:set trusted_domains 2 --value=cloud.example.com, using the next free index, which occ config:system:get trusted_domains shows.
Because the hostname your reverse proxy passes on is not in trusted_domains, and in the official Docker image the variable you probably used to set it, NEXTCLOUD_TRUSTED_DOMAINS, is read once, during the automated install, and never again. Editing the compose file and recreating the container changes nothing. Add the name with occ, which writes it into config.php:
docker compose exec -u www-data app php occ config:system:get trusted_domains
docker compose exec -u www-data app php occ config:system:set trusted_domains 2 --value=cloud.example.comUse the next free index. The first command prints the list one entry per line, and the first line is index 0. Setting an index that already exists replaces that entry: in our test, writing index 1 removed the domain that had been there, and that name started getting the error page instead.
Two answers circulate, and the popular one is wrong in both directions#
The first answer is "set NEXTCLOUD_TRUSTED_DOMAINS". It is right only before the first start. The image's docker-entrypoint.sh handles the variable inside its install block, which runs when the volume holds no installed Nextcloud, and only when NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD and a database are all set. If you finished the setup in the browser instead, the variable was never applied at all.
The second answer extends the first: "all of the proxy variables are install time only, so use occ for everything." That is also wrong, the other way. TRUSTED_PROXIES, OVERWRITEPROTOCOL, OVERWRITEHOST, OVERWRITECLIURL and OVERWRITEWEBROOT are read by reverse-proxy.config.php, a file the image copies into config/, which calls getenv() on every request. Change those in compose, recreate the container, and the new value applies at once.
What the README does say is narrower: "once set at install time, removing these environment variables later won't remove them from your config/config.php". The installer writes the merged configuration into config.php. So removing a variable does not bring back auto-detection; it brings back the install-time value.
What each variable does after install, measured#
We installed nextcloud:apache (Nextcloud 35.0.2) with SQLite through the automated install, with NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com, TRUSTED_PROXIES=172.16.0.0/12 and OVERWRITEPROTOCOL=https, then changed each variable and recreated the container with docker compose up -d.
| Change after install | What Nextcloud did |
|---|---|
NEXTCLOUD_TRUSTED_DOMAINS changed to new.example.com | Ignored. No "Setting trusted domains" line in the log; new.example.com got HTTP 400 and the untrusted domain page, the old name still worked |
OVERWRITEPROTOCOL changed from https to http | Applied. config:system:get returned http, the login redirect switched to http://, while config.php still said https |
OVERWRITEPROTOCOL and TRUSTED_PROXIES set to empty | The install-time values came back: https and 172.16.0.0/12, read from config.php |
occ config:system:set overwriteprotocol --value=http while the variable said https | occ printed "System config value overwriteprotocol set to string http" and exited 0. config.php changed. The running value stayed https |
occ config:system:set trusted_domains 1 --value=new.example.com | Applied at once, no restart. Replaced cloud.example.com at index 1 |
The fourth row is the trap that keeps forum threads going. When a key is set by a variable, the image's config file is merged over config.php, so occ writes a value nobody reads and reports success. Rule of thumb: a key you set with an environment variable is changed in compose; a key you did not is changed with occ. occ config:list system shows what is actually in force, which is why the README recommends it over reading config.php.
The install also left overwrite.cli.url at https://localhost (http://localhost without the protocol variable). Nextcloud's installer builds it from the address of the install request. Set it to the public URL, because the reverse proxy page of the admin manual says it is the base for URLs generated "using any kind of command line tools", such as the notifications area.
Why the hostname is untrusted in the first place#
Nextcloud's installer, Setup.php, writes one trusted domain: the host of the request that ran the install. With the browser wizard, that is whatever was in the address bar, for example 192.168.1.10:8080 if the proxy was not built yet. With the automated install in Docker it is localhost, and NEXTCLOUD_TRUSTED_DOMAINS adds entries from index 1. Then the proxy goes up, forwards Host: cloud.example.com, and Nextcloud answers HTTP 400 with "Access through untrusted domain".
How the check matches, from TrustedDomainHelper.php and our requests:
- Ports are ignored.
cloud.example.com:8443passed with onlycloud.example.comin the list. - Localhost is always allowed, whatever the list says.
- Wildcards work.
*.example.orgaccepteda.example.organda.b.example.org, and refusedexample.org. overwritehostswitches the check off. The code comment reads "overwritehost is always trusted", and a request withHost: evil.example.netgot a redirect, not the error. Many threads fix the error this way. It works, and it removes the protectionconfig.sample.phpsays trusted domains exist for, which is "host header poisoning".
If the proxy runs with a trusted proxy configured, Nextcloud checks X-Forwarded-Host instead of Host. Caddy and Traefik send the same name in both, so the entry is the public hostname either way.
What each proxy sends, and what Nextcloud needs from it#
We put Nextcloud behind Caddy 2.11.7 and Traefik 3.7.16 with no proxy settings at all, then pointed each at traefik/whoami to record the headers. For Nginx Proxy Manager 2.16.0 the headers come from the proxy.conf that every generated proxy host includes.
| Caddy 2.11.7 | Traefik 3.7.16 | Nginx Proxy Manager 2.16.0 | |
|---|---|---|---|
Passes Host unchanged | Yes | Yes | Yes |
X-Forwarded-Proto / -For | Yes / yes | Yes / yes | Yes / yes |
X-Forwarded-Host | Yes | Yes | No |
X-Real-IP | No | Yes | Yes |
| Login redirect with nothing set | http:// | http:// | not tested |
trusted_domains | the public hostname | the public hostname | the public hostname |
trusted_proxies | Caddy's container IP or its network's CIDR | Traefik's container IP or CIDR | NPM's container IP or CIDR |
overwriteprotocol | https | https | https |
overwrite.cli.url | https://cloud.example.com | same | same |
Client IP in Apache's log without trusted_proxies | Caddy's address | the client's | not tested |
| Extra from the admin manual | redir /.well-known/carddav /remote.php/dav/ 301, same for caldav | redirectregex middleware for .well-known | proxy_hide_header Upgrade; in Custom Nginx Configuration, or iOS clients get "Connection Closed" |
The redirect row is why trusted_proxies matters even when the domain error is gone. Nextcloud's Request.php believes X-Forwarded-Proto only when the request comes from an address in trusted_proxies; otherwise it sees plain HTTP from the proxy and builds http:// links. Either fix worked in our test: setting trusted_proxies to the proxy network, or setting overwriteprotocol to https, made the redirect https://. Set both. overwriteprotocol keeps the links right if the proxy's address changes, and trusted_proxies is what lets Nextcloud read the real client from the proxy's headers, which the admin manual says it uses "for access control and logging".
The client IP row comes from the image, not Nextcloud. The README says the Apache image replaces the remote address with X-Real-IP for any proxy in 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16. Traefik and NPM send that header, Caddy does not, so behind Caddy Apache logged the proxy. If you want Nextcloud to take the client from X-Forwarded-For instead, the README's instruction is to set APACHE_DISABLE_REWRITE_IP=1 and list the proxy in TRUSTED_PROXIES. Why container logs show 172.17.0.1, not the client IP explains why trusting a whole Docker subnet can let a client forge that header.
The settings, in one block#
If you manage them in compose, this is the whole set for a proxy on the same Docker network:
services:
app:
image: nextcloud:apache
environment:
TRUSTED_PROXIES: 10.201.14.0/24
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://cloud.example.comReplace the CIDR with the proxy's real network. docker network inspect <network> -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}' prints it. Do not copy 172.16.0.0/12 from a thread: on our test host Docker allocated 10.201.x.x networks, so that range matched nothing. Docker subnet clashes and address pools covers where those addresses come from. The admin manual lists addresses and CIDR ranges as the accepted forms for trusted_proxies, so a container name is not one of them, and the Docker README says CIDR notation is supported for IPv4.
The trusted domain still goes in with occ, as above, because no variable can add it after install.
When this answer stops applying#
- Not the official image. The variable behaviour above belongs to the official
nextcloudimage's entrypoint and itsreverse-proxy.config.php. Theocccommand and theconfig.phpkeys are Nextcloud's own and apply to any install. - An older volume. The README warns that the image's config files in
config/are not overwritten on upgrade. It says an outdated copy "may cause certain auto configuration environment variables to be ignored", and the startup log warns that the file "differs from the latest version of this image". - Nextcloud served under a path. Add
overwritewebroot, as in the admin manual's subdirectory example. - We did not test the FPM image with a separate web server, IPv6 proxy addresses, or NPM against a live Nextcloud.
What to do next#
Run occ config:system:get trusted_domains, add the public hostname at the next index, set TRUSTED_PROXIES, OVERWRITEPROTOCOL and OVERWRITECLIURL in compose, recreate, and check the result with occ config:list system. If the proxy itself returns an error before Nextcloud answers, Why Nginx Proxy Manager returns 502 Bad Gateway reads the log line for you, and Reverse proxy and TLS has the rest of the proxy setup. If this is the moment you are wondering whether Nextcloud is worth the upkeep, OpenCloud vs Nextcloud and Nextcloud vs Seafile weigh the two lighter file servers against it, and the Nextcloud profile lists its other traps.
Questions#
Why did changing NEXTCLOUD_TRUSTED_DOMAINS in docker-compose.yml not work?
The image's entrypoint runs occ config:system:set trusted_domains for each listed domain only inside its install step, which runs once, when the volume holds no installed Nextcloud. After that the variable is never read. We recreated the container with a new value and the new hostname still got the untrusted domain page. Add it with occ instead, or edit config/config.php in the volume.
Can I use a wildcard in Nextcloud trusted_domains?
Yes. Nextcloud turns * into a pattern matching letters, digits, dots and hyphens. In our test *.example.org accepted a.example.org and a.b.example.org but refused the bare example.org, which needs its own entry. A port in the request is ignored when matching, so cloud.example.com also accepts cloud.example.com:8443.
Should I set overwritehost to fix the untrusted domain error?
Only if you want every generated link to use one hostname. Nextcloud's code treats overwritehost as always trusted, so once it is set the untrusted domain check is off: our test request with Host: evil.example.net got a redirect instead of the error page. Add the real name to trusted_domains and leave overwritehost unset unless you need one canonical public URL.
Why does Nextcloud redirect to http behind my HTTPS proxy?
Nextcloud believes the proxy's X-Forwarded-Proto: https only when the request comes from an address in trusted_proxies. Without that, it sees plain HTTP from the proxy and builds http:// links. Behind Caddy and Traefik with nothing set, our login redirect pointed at http://. Setting either trusted_proxies to the proxy's address or overwriteprotocol to https fixed it.
Where do I run occ in the Nextcloud Docker image?
Inside the app container, as the web server user, which is www-data (UID 33) in the official image: docker compose exec -u www-data app php occ config:list system. The README recommends config:list system over reading config.php, because it shows the merged configuration including the image's extra config files, with secrets left out.
Sources#
- nextcloud/docker README (trusted domains, reverse proxy variables, occ)
- nextcloud/docker, docker-entrypoint.sh (install step, NEXTCLOUD_TRUSTED_DOMAINS)
- nextcloud/docker, reverse-proxy.config.php
- Nextcloud admin manual, reverse proxy configuration
- Nextcloud server, config.sample.php (trusted_domains, trusted_proxies)
- Nextcloud server, Request.php (protocol and host detection)
- Nextcloud server, TrustedDomainHelper.php
- Nextcloud server, Setup.php (values written by the installer)
- Caddy documentation, reverse_proxy headers
- Nginx Proxy Manager, include/proxy.conf
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.