NextCloud with two reverse proxies

I’ve recently started running NextCloud on my home LAN. To make it easier to manage, I decided to go with the AIO route, so I can leverage Docker on a Raspberry Pi device running Ubuntu Server.

Because I don’t have a static public IP address at home, I decided to go with a Cloudflare ZeroTrust tunnel as a reverse proxy. This does come with a couple of caveats, bit it’s good enough for my purposes. To set up the tunnel I deployed a cloudflared container on my Docker server and set up a Public Hostname in it pointing to http port 11000 on my Docker server. This is all pretty standard stuff.

The small annoyance

This NextCloud service is on my home LAN, but proxied via Cloudflare. This means that I go out onto the Internet to Cloudflare and back in again via the tunnel to reach it even from my home LAN.

The speed is meh, especially when working with files via stuff like LibreOffice using WebDAV or uploading photos to NextCloud from my phone.

The solution

The basic idea for a solution is to have a second reverse proxy on the Docker host that listens on the https port and proxies to http on port 11000 where the NextCloud Apache server is listening. I then arrange for stuff on my home LAN to see the NextCloud site as the internal IP of the Docker server instead of seeing it as Cloudflare.

There are three elements involved in this. Let’s look at them one at a time.

DNS

For LAN DNS, I decided to adapt my Using dnsmasq on Ubuntu or Mint ugly hack into a LAN-wide DNS server handed out by DHCP. It’s pretty easy to spin up a dnsmasq docker container, but first you have to stop the Docker server running systemd-resolved. This time I decided to disable it by creating a file /etc/systemd/resolved.conf.d/noresolved.conf with the following contents:

[Resolve]
DNSStubListener=no

I then ran sudo systemctl restart systemd-resolved.service and my systemd-resolved tears dried up.

Beware! The below used to show a config where dnsmasq was inside a container on the Docker host, this broke stuff. If you followed that example, please delete the dnsmasq container and do the bit below here instead.

The next step is to put a temporary /etc/resolv.conf in place:

sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf

Next I installed dnsmasq and set up /etc/dnsmasq.conf to look something like this:

no-resolv
# Use Cloudflare as default nameservers
server=1.0.0.1
server=1.1.1.1
strict-order

# Corporate domains
# work.local has DNS at 192.168.1.1 and 192.168.1.2
server=/work.local/192.168.1.1 
server=/work.local/192.168.1.2

# Me no likey WPAD
address=/wpad/
address=/wpad.local/
address=/wpad.work.local/

# NextCloud on my Docker server
address=/nextcloud.example.com/10.20.30.40

Then I edited /etc/resolv.conf and put 127.0.0.1 in as the nameserver.

The last step was configuring my DHCP server to pass 10.20.30.40 as my primary DNS server.

At this point it may be a good idea to reboot your Docker server. After the reboot, try this command to make sure DNS still resolves inside your containers:

sudo docker exec -t nextcloud-aio-nextcloud nslookup google.com

SSL certificate

The basic idea is to get an SSL certificate for nextcloud.example.com, in my case I decided to go with Let’s Encrypt, running certbot on my Docker server. How to get certbot set up and get a certificate is very much dependent on stuff like who you use as a DNS provider, so it’s out of the scope of this post.

My DNS is hosted at Cloudflare, so I installed the certbot and python3-certbot-dns-cloudflare packages and used the certbot certonly command with appropriate parameters to get my certificate under /etc/letsencrypt on my Docker server.

The reverse proxy

Now we get to the meat of it. I decided to go with an nginx reverse proxy, locking the feature set down at version 1.21.6. I could have gone with Traefik, but that seemed like overkill for a simple single site reverse proxy.

This is based on an nginx example at https://github.com/nextcloud/all-in-one/discussions/588

I created a directory for docker compose and changed into it.

mkdir ~/intproxy
cd ~/intproxy

Next I created a ./docker-compose.yml with with this in it:

services:
  nginx:
    container_name: nginx
    image: nginx:1.21.6
    restart: always
    ports:
      - 443:443
    volumes:
      # - ./nginx:/etc/nginx
      - /etc/letsencrypt:/etc/letsencrypt

Note how the /etc/nginx volume is commented out, this is for a reason.

Next I ran the container, grabbed the default nginx config out of it, and stopped it:

sudo docker compose up -d
sudo docker cp nginx:/etc/nginx nginx
sudo docker compose down

Now it was time to edit the nginx configuration files (some permissions massaging may have been needed, but I actually just used sudo vim to edit these files), starting with ./nginx/conf.d/default.conf as follows:

server {
    listen      443 ssl http2;
    listen [::]:443 ssl http2;
    server_name nextcloud.example.com;

    location / {
        # The host ip address
        proxy_pass http://10.20.30.40:11000; 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 0;

        # Websocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
    # Managed by certbot on host
    ssl_certificate /etc/letsencrypt/live/nextcloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nextcloud.example.com/privkey.pem;
}

Next I did ./nginx/nginx.conf as follows:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    keepalive_timeout  65;

    # Connection header for WebSocket reverse proxy
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    include /etc/nginx/conf.d/*.conf;
}

That concludes the nginx config, so then I changed # ./nginx:/etc/nginx to ./nginx:/etc/nginx in my docker-compose.yml and we’re good to go.

I stared up my reverse proxy with sudo docker compose up -d and my NextCloud site was available and way faster from my LAN.

Sooo… Did it work?

As we used to say YMMV… In my setup, opening and saving a spreadsheet with LibreOffice Calc using WebDAV used to take between 4 times and 10 times as long as it takes now.


Posted

in

,

by