Nginx reverse proxy not working for seafhttp

Good evening everyone,
I installed seafile-docker on a raspi5 as a home server. The docker container contains a nginx reverse proxy that splits the traffic for seahub and seafhttp. Everything is working fine, but now I want to use another nginx reverse proxy on an external server that has its own domain and is accessible over the internet. Both are connected via a wireguard vpn.

I installed nginx on the external server using this configuration:

server {
    server_name subdomain.domain.net;
	listen 80;
	client_max_body_size 10m;

    location / {
        proxy_pass http://192.168.200.5:8081;  # The ip of the home server.
		proxy_set_header Host $http_host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
			    
        proxy_set_header Forwarded "for=$remote_addr;proto=$scheme";
        proxy_set_header Connection "";
        proxy_http_version 1.1;

        access_log      /var/log/nginx/seafile.access.log;
        error_log       /var/log/nginx/seafile.error.log info;
    }
}

Then I changed SERVICE_URL, CSRF_TRUSTED_ORIGINS and FILE_SERVER_ROOT in seahub_settings.py on my home server:

# -*- coding: utf-8 -*-
SECRET_KEY = <secret>
SERVICE_URL = "hxxp://subdomain.domain.net"  # The domain of the external server

CSRF_TRUSTED_ORIGINS = [
    'hxxp://subdomain.domain.net'  # The domain of the external server
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'seahub_db',
        'USER': 'seafile',
        'PASSWORD': <secret>,
        'HOST': 'db',
        'PORT': '3306',
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

CACHES = {
    'default': {
        'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
        'LOCATION': 'memcached:11211',
    },
    'locmem': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    },
}
COMPRESS_CACHE_BACKEND = 'locmem'
TIME_ZONE = 'Etc/UTC'
FILE_SERVER_ROOT = "hxxp://subdomain.domain.net/seafhttp"  # The domain of the external server

Now I can access seafile via hxxp://subdomain.domain.net with a web browser, browse the libraries, download files and so on.

But if I try to upload a little text file (<1kB) via the web browser, the upload fails. I opened the browsers DevTools and saw that a POST Request to
hxxp://subdomain.domain.net/seafhttp/upload-aj/8c4f438a-a28a-4ccd-8622-26908bad872d?ret-json=1 failed with 504 Gateway Time-out.

The seafile.error.log on the external server says:

2025/07/10 20:53:59 [error] 15443#15443: *15 upstream prematurely closed connection while reading response header from upstream, client: 92.72.xxx.xxx, server: subdomain.domain.net, request: "POST /seafhttp/upload-aj/35ca7b54-5800-4507-8991-9f823ab158e7?ret-json=1 HTTP/1.1", upstream: "http://192.168.200.5:8081/seafhttp/upload-aj/35ca7b54-5800-4507-8991-9f823ab158e7?ret-json=1", host: "subdomain.domain.net", referrer: "hxxp://subdomain.domain.net/library/11992eee-70cc-40dd-bfb8-665a5c333341/Meine%20Bibliothek/"

Why did that happen? All other requests are forwarded correctly by the external and the internal nginx to seahub inside the container. Only POST requests to seafhttp fail.

I opened a shell on the external server and sent the failed POST request using curl to the url shown in the browsers DevTools:

curl -X POST "hxxp://subdomain.domain.net/seafhttp/upload-aj/8c4f438a-a28a-4ccd-8622-26908bad872d?ret-json=1" \
  -H "Accept: application/json, text/javascript, */*; q=0.01" \
  -H "Origin: hxxp://subdomain.domain.net" \
  -H "Referer: hxxp://subdomain.domain.net/library/11992eee-70cc-40dd-bfb8-665a5c333341/Meine%20Bibliothek/" \
  -H "Cookie: sfcsrftoken=KEetmxleEqNFVXxHcBOp3EDk27xtxXWI; sessionid=yga10pila7la4oujh4z95pttvr32mizg" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" \
  -F "file=@/root/testupload.txt" \
  -F "parent_dir=/"

This worked! The file uploaded with curl appears in the seafile library.

So everything seems to work fine, except upload requests to seafhttp that have been forwarded by both nginx instances. Since direct requests to the home server do work, i think there must be something wrong with the externals nginx configuration shown above…

Does anyone know a solution for this?
Thank you,
Volker

I tried something new, and now it’s getting really wired:

I sent the upload request with curl from a shell of another linux server that has nothing to with my setup here. It worked - I expected that.

Then I sent the curl request from a local linux machine behind my local internet connection (vodafone, germany) and it failed! How is this possible?
How can my seafile/nginx setup distinguish which internet connection i use? Something to do with DS Lite?

The nginx log of the external server says:

2025/07/11 16:53:03 [error] 15443#15443: *714 upstream prematurely closed connection while reading response header from upstream, client: 92.72.xxx.xxx, server: subdomain.domain.net, request: "POST /seafhttp/upload-aj/36e03db8-9747-47e2-8ff8-1d520dbe1940?ret-json=1 HTTP/1.1", upstream: "http://192.168.200.5:8081/seafhttp/upload-aj/36e03db8-9747-47e2-8ff8-1d520dbe1940?ret-json=1", host: "subdomain.domain.net", referrer: "hxxp://subdomain.domain.net/library/11992eee-70cc-40dd-bfb8-665a5c333341/Meine%20Bibliothek/"

Just a guess but when you try to curl to subdomain.domain.net from your local network, I suspect you are getting your public IP from DNS. So you are trying to talk to your own public IP. A lot of routers don’t treat that the same as traffic coming in from outside, because it didn’t come from outside (the packet came into an internal interface, so it didn’t go through the firewall and NAT rules like packets that came from the internet interface do).

Your router might have an option for “NAT reflection” or “hairpin NAT” that creates extra rules to let internal addresses access the NAT ports on your public IP.

You might also be able to modify your internal DNS to override the lookups for subdomain.domain.net and return the internal address for the server to just kinda side-step your router. But this can be harder unless you already have your own internal DNS server set up.