Seafile as a docker container behind NGINX as a reverse proxy

I have a machine with various Docker containers. One of them is Seafile. Because I want to use port 443 for all containers, I cannot assign it to the Seafile container. But I don’t want to use any other port.

So I wanted to use NGINX as a reverse proxy for all my containers. Except for Seafile, I can configure this with all containers by using proxy_pass http://127.0.0.1:.

Unfortunately, it doesn’t work like this with Seafile.

The following ports are configured in the compose.yaml file:

  • “8001:8000” # media server
  • “8082:8082” # seafile server
  • “8083:8083” # notification server

With Apache, I can create a reverse proxy configuration that works by using the IP address of the machine. Why, I don’t understand. I get proxy errors when I use http://127.0.0.1 in the Apache reverse proxy configuration. It works with the machine IP address. Of course I had to adjust it accordingly in the gunicorn.conf.py file (127.0.0.1 → 0.0.0.0), but Seafile works with Apache as a reverse proxy this way.

But it doesn’t work with NGINX as a reverse proxy. Neither with 127.0.0.1, nor with the IP address of the machine. I always get the error 502. I don’t understand it.

Can someone help me unravel the knot in my head?

Are you using NPM or just pure nginx? In either case, you can try the name of the container and make sure they are on the same Docker network. Let me know if this helps!

No, NPM is not involved.

Using the container name unfortunately doesn’t work, but the IP address of the docker0 interface works. Thank you!

What is the reason that it works for other containers with the address 127.0.0.1, but for the Seafile container you have to use the IP address of the Docker interface?
In the Seafile configuration file gunicorn.conf.py I still have to change the binding from 127.0.0.1:8000 to 0.0.0.0:8000 for Seafile to work.

For most Docker containers, 127.0.0.1 works in the context of the host machine when mapped via proxy_pass http://127.0.0.1:<port>. This is because the container’s services are explicitly bound to 127.0.0.1 or 0.0.0.0 (making them accessible through the loopback or all interfaces).

Seafile’s default configuration binds its services (e.g., gunicorn) to 127.0.0.1. But inside the Docker container, 127.0.0.1 refers to the container’s internal loopback interface, not the host’s loopback. This makes the service unreachable from the host using 127.0.0.1.

By contrast, the docker0 interface on the host (typically 172.17.0.1 or similar) connects Docker containers to the host, allowing you to access Seafile’s service via the Docker interface IP.

When you update gunicorn.conf.py to bind Seafile to 0.0.0.0, it listens on all network interfaces, including the container’s IP and the Docker bridge network. This change allows NGINX (or any other reverse proxy) to communicate with Seafile via the Docker bridge IP.

I might add, if you have nginx proxy manager on the same machine, this becomes a trivial issue (still fairly simple if on another machine btw) as you can use the container name and port instead of an IP, but I can understand not wanting a whole new piece introduced to solve a simple issue. I will add though, NPM is a great reverse proxy and I replaced traefik with NPM due to it being so much easier to use, even though I lost some features such as custom middleware in the process. If this seems interesting to you, definitely look into it!

Also for transparency, I used chatgbt along with some documentation to provide this information.

Apache likely uses the machine’s external IP or docker0 interface IP rather than relying on 127.0.0.1. When you adjust the Seafile service to bind to 0.0.0.0, Apache can communicate with it through any interface accessible from the host, resolving the issue.

Hi,
If either @Jukelyn or @dbet1 could share their nginx config file and the ports and ssl configuration in the compose.yaml, I would be very thankful. Especially, if you also use the webdav (/seafdav) feature.

Here is my nginx configuration:

log_format seafileformat '$http_x_forwarded_for $remote_addr [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $upstream_response_time';
map $http_upgrade $connection_upgrade {
	default upgrade;
	'' close;
}

server {
	listen 80;
	server_name seafile-test.domain.tld;
	rewrite ^ https://$http_host$request_uri? permanent;    # Forced redirect from HTTP to HTTPS
	server_tokens off;      # Prevents the Nginx version from being displayed in the HTTP response header
	}

server {
	listen 443 ssl http2;
	include snippets/snakeoil.conf;
	server_name seafile-test.domain.tld;
	server_tokens off;
	client_max_body_size 10m;

	location / {
		proxy_pass         http://172.17.0.1:8001/;
		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_read_timeout 1200s;
		proxy_connect_timeout	75s;
		proxy_http_version 1.1;

		proxy_set_header X-Forwarded-Proto $scheme;
		proxy_set_header Forwarded "for=$remote_addr;proto=$scheme";
		proxy_set_header Connection "";

		# used for view/edit office file via Office Online Server
		client_max_body_size 0;

		access_log      /var/log/nginx/seahub.access.log seafileformat;
		error_log       /var/log/nginx/seahub.error.log;
    }

	location /seafhttp {
		rewrite ^/seafhttp(.*)$ $1 break;
		proxy_pass http://172.17.0.1:8082;
		client_max_body_size 0;
		proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_connect_timeout  36000s;
		proxy_read_timeout  36000s;
		proxy_send_timeout  36000s;
		proxy_http_version 1.1;

		# Large file uploads
		proxy_request_buffering off;

		access_log      /var/log/nginx/seafhttp.access.log seafileformat;
		error_log       /var/log/nginx/seafhttp.error.log;
	}

	location /media {
		proxy_pass http://172.17.0.1:8001/media;
		proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_http_version 1.1;
		client_max_body_size 0;
		access_log /var/log/nginx/media.access.log;
		error_log  /var/log/nginx/media.error.log;
	}

	location /notification/ping {
		proxy_pass http://172.17.0.1:8083/ping;
		access_log /var/log/nginx/notification.access.log;
		error_log  /var/log/nginx/notification.error.log;
	}

	location /notification {
		proxy_pass http://172.17.0.1:8083;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		access_log /var/log/nginx/notification.access.log;
		error_log  /var/log/nginx/notification.error.log;
	}
}

In the compose.yaml I don’t have a ssl configuration. There are three open ports:

  • 8001:8000
  • 8082:8082
  • 8083:8083

I don’t use webdav.

Here is my config. I should note, my main server that my reverse proxy is on is on another machine and so I route to my other machine were Seafile is hosted with that port.

# ------------------------------------------------------------
# seafile.jukelyn.com
# ------------------------------------------------------------



# ------------------------------------------------------------
# seafile.jukelyn.com
# ------------------------------------------------------------



# Required for OnlyOffice DocumentServer
map $http_x_forwarded_proto $the_scheme {
    default $http_x_forwarded_proto;
    "" $scheme;
}

map $http_x_forwarded_host $the_host {
    default $http_x_forwarded_host;
    "" $host;
}

map $http_upgrade $proxy_connection {
    default upgrade;
    "" close;
}

server {
    listen 80;
    server_name seafile.jukelyn.com;
    rewrite ^ https://$http_host$request_uri? permanent;
    server_tokens off;
}

server {
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/npm-1/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/npm-1/privkey.pem;
    server_name seafile.jukelyn.com;

    proxy_set_header X-Forwarded-For $remote_addr;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
    server_tokens off;

    #
    # Seafile
    #
    location / {
        proxy_pass http://192.168.1.148:10080/;
        proxy_read_timeout 310s;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Connection "";
        proxy_http_version 1.1;

        client_max_body_size 0;
    }

    #
    # OnlyOffice DocumentServer Subfolder
    #
    location /onlyofficeds/ {
        proxy_pass http://192.168.1.148:20080/;

        proxy_http_version 1.1;
        client_max_body_size 100M; # Limit Document size to 100MB
        proxy_read_timeout 3600s;
        proxy_connect_timeout 3600s;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $proxy_connection;

        # Ensure proper subfolder handling
        proxy_set_header X-Forwarded-Host $the_host/onlyofficeds;
        proxy_set_header X-Forwarded-Proto $the_scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

SSL is handled by nginx, I’m not using the SEAFILE_SERVER_LETSENCRYPT option.

services:
  db:
    image: mariadb:10.11
    container_name: seafile-mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}  # Required, set the root's password of MySQL service.
      - MYSQL_LOG_CONSOLE=true
      - MARIADB_AUTO_UPGRADE=1
    volumes:
      - ./config/seafile-mysql/db:/var/lib/mysql  # Required, specifies the path to MySQL data persistent store.
    networks:
      - seafile-net

  memcached:
    image: memcached:1.6.18
    container_name: seafile-memcached
    entrypoint: memcached -m 256
    networks:
      - seafile-net

  elasticsearch:
    image: elasticsearch:8.13.0
    container_name: seafile-elasticsearch
    environment:
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
      - "xpack.security.enabled=false"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    mem_limit: 4g
    volumes:
      - ./config/seafile-elasticsearch/data:/usr/share/elasticsearch/data  # Required, specifies the path to Elasticsearch data persistent store.
    networks:
      - seafile-net
          
  seafile:
    image: docker.seadrive.org/seafileltd/seafile-pro-mc:11.0-latest
    container_name: seafile
    ports:
      - "10080:80"
      # - "10443:443"  # If https is enabled, cancel the comment.
    volumes:
      - ./config/seafile-data:/shared  # Required, specifies the path to Seafile data persistent store.
    environment:
      - DB_HOST=db
      - DB_ROOT_PASSWD=${DB_ROOT_PASSWD}  # Required, the value should be root's password of MySQL service.
      - TIME_ZONE=${TIME_ZONE}
      - SEAFILE_ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL}
      - SEAFILE_ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD}
      - SEAFILE_SERVER_LETSENCRYPT=${SEAFILE_SERVER_LETSENCRYPT}  # Whether to use https or not
      - SEAFILE_SERVER_HOSTNAME=${SEAFILE_SERVER_HOSTNAME}  # Specifies your host name if https is enabled
    depends_on:
      - db
      - memcached
      - elasticsearch
    networks:
      - seafile-net

networks:
  seafile-net:

Interesting: I go to ports 8000, 8082 and 8083 and thus bypass NGINX within the container. Jukelyn goes via port 80 within the container and thus uses NGINXi in the container.

Both work.