Docker: how to change the path in the webroot?

So, currently I am working on a setup based on this example. I have customized it to my needs in order to make SSL work (give it the domain, an email address etc.) and changed some container and volume names.

What do I need to do to make Nextcloud run on https://domain.tld/nextcloud/, instead of https://domain.tld/? I would run it on its separate domain, but I don’t have the possibility to, and I want to run more things on the same domain, so, this is the method I am resorting to.

I did test my setup normally, with Nextcloud running in the webroot, and everything works like it should.

  • When I am writing domain.tld I am referring to the actual domain I own
  • When I refer to container, I am referring to trying to directly access the web container by opening a port on it with ports: - "8080:80" in the docker-compose.yml file

Here is what I tried:

  1. Give the web (in my file, nextcloud-web) container the VIRTUAL_PATH=/nextcloud variable. Of course, this needs to be done in order to make the nginxproxy/nginx-proxy container actually proxy the container to the /nextcloud path, according to their READMEs and wikis, so it was like this throughout all these steps.

  2. Give the app (in my file, nextcloud) container the OVERWRITEWEBROOT=/nextcloud variable. I remember I had a setup working where this was required, so this was a good first step I guess, but, I think there’s more configuring needed to happen to make this work.

  • Results on domain.tld/nextcloud: 502 from nginx
  • Results on container/: 502 from nginx
  • Results on container/nextcloud: something that looks like the Nextcloud admin account creation page with everything but the HTML all stripped off of it:

  1. Try to modify the web/nginx.conf file the best I could. Here is what I came up with:
worker_processes auto;

error_log  /var/log/nginx/error.log warn;
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;
    #tcp_nopush     on;

    # Prevent nginx HTTP Server Detection
    server_tokens   off;

    keepalive_timeout  65;

    #gzip  on;

    upstream php-handler {
        server app:9000;
    }

    server {
        listen 80;

        # HSTS settings
        # WARNING: Only add the preload option once you read about
        # the consequences in https://hstspreload.org/. This option
        # will add the domain to a hardcoded list that is shipped
        # in all major browsers and getting removed from this list
        # could take several months.
        #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;

        # set max upload size
        client_max_body_size 512M;
        fastcgi_buffers 64 4K;

        # Enable gzip but do not remove ETag headers
        gzip on;
        gzip_vary on;
        gzip_comp_level 4;
        gzip_min_length 256;
        gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
        gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

        # Pagespeed is not supported by Nextcloud, so if your server is built
        # with the `ngx_pagespeed` module, uncomment this line to disable it.
        #pagespeed off;

        # HTTP response headers borrowed from Nextcloud `.htaccess`
        add_header Referrer-Policy                      "no-referrer"   always;
        add_header X-Content-Type-Options               "nosniff"       always;
        add_header X-Download-Options                   "noopen"        always;
        add_header X-Frame-Options                      "SAMEORIGIN"    always;
        add_header X-Permitted-Cross-Domain-Policies    "none"          always;
        add_header X-Robots-Tag                         "none"          always;
        add_header X-XSS-Protection                     "1; mode=block" always;

        # Remove X-Powered-By, which is an information leak
        fastcgi_hide_header X-Powered-By;

        # Path to the root of your installation
        root /var/www/html;

        # Specify how to handle directories -- specifying `/index.php$request_uri`
        # here as the fallback means that Nginx always exhibits the desired behaviour
        # when a client requests a path that corresponds to a directory that exists
        # on the server. In particular, if that directory contains an index.php file,
        # that file is correctly served; if it doesn't, then the request is passed to
        # the front-end controller. This consistent behaviour means that we don't need
        # to specify custom rules for certain paths (e.g. images and other assets,
        # `/updater`, `/ocm-provider`, `/ocs-provider`), and thus
        # `try_files $uri $uri/ /index.php$request_uri`
        # always provides the desired behaviour.
        index index.php index.html /nextcloud/index.php$request_uri;

        # Rule borrowed from `.htaccess` to handle Microsoft DAV clients
        location = /nextcloud/ {
            if ( $http_user_agent ~ ^DavClnt ) {
                return 302 /nextcloud/remote.php/webdav/$is_args$args;
            }
        }

        location = /nextcloud/robots.txt {
            allow all;
            log_not_found off;
            access_log off;
        }

        # Make a regex exception for `/.well-known` so that clients can still
        # access it despite the existence of the regex rule
        # `location ~ /(\.|autotest|...)` which would otherwise handle requests
        # for `/.well-known`.
        location ^~ /nextcloud/.well-known {
            # The rules in this block are an adaptation of the rules
            # in `.htaccess` that concern `/.well-known`.

            location = /nextcloud/.well-known/carddav { return 301 /nextcloud/remote.php/dav/; }
            location = /nextcloud/.well-known/caldav  { return 301 /nextcloud/remote.php/dav/; }

            location /nextcloud/.well-known/acme-challenge    { try_files $uri $uri/ =404; }
            location /nextcloud/.well-known/pki-validation    { try_files $uri $uri/ =404; }

            # Let Nextcloud's API for `/.well-known` URIs handle all other
            # requests by passing them to the front-end controller.
            return 301 /nextcloud/index.php$request_uri;
        }

        # Rules borrowed from `.htaccess` to hide certain paths from clients
        location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
        location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }

        # Ensure this block, which passes PHP files to the PHP process, is above the blocks
        # which handle static assets (as seen below). If this block is not declared first,
        # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
        # to the URI, resulting in a HTTP 500 error response.
        location ~ \.php(?:$|/) {
            # Required for legacy support
            rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy) /nextcloud/index.php$request_uri;

            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            set $path_info $fastcgi_path_info;

            try_files $fastcgi_script_name =404;

            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $path_info;
            #fastcgi_param HTTPS on;

            fastcgi_param modHeadersAvailable true;         # Avoid sending the security headers twice
            fastcgi_param front_controller_active true;     # Enable pretty urls
            fastcgi_pass php-handler;

            fastcgi_intercept_errors on;
            fastcgi_request_buffering off;
        }

        location ~ \.(?:css|js|svg|gif)$ {
            try_files $uri /nextcloud/index.php$request_uri;
            expires 6M;         # Cache-Control policy borrowed from `.htaccess`
            access_log off;     # Optional: Don't log access to assets
        }

        location ~ \.woff2?$ {
            try_files $uri /nextcloud/index.php$request_uri;
            expires 7d;         # Cache-Control policy borrowed from `.htaccess`
            access_log off;     # Optional: Don't log access to assets
        }

        # Rule borrowed from `.htaccess`
        location /nextcloud/remote {
            return 301 /nextcloud/remote.php$request_uri;
        }

        location /nextcloud/ {
            try_files $uri $uri/ /nextcloud/index.php$request_uri;
        }
    }
}

What I did was basically try to replace everything I thought would make Nginx broadcast to the custom webroot path (not very smart).

  • Results on domain.tld/nextcloud: 404 from nginx

  • Results on container/: the normal Nextcloud admin account creation page, not tested further than this:

  • Results on container/nextcloud: 404 from nginx

  1. Try all of the above at the same time.
  • Results on domain.tld/nextcloud: 404 from nginx
  • Results on container/: same thing with the stripped down Nextcloud admin account creation page
  • Results on container/nextcloud: 404 from nginx

I have some basic knowledge about Nginx, enough to get me by with simple configurations and to understand most of the configuration file, and I also know how to use Docker and docker-compose. I do have some experience with Nextcloud, running on bare metal and also through containers. I had a setup like this working through Docker an year or two ago, but it wasn’t using docker-compose and it required some configuring after spinning up the containers, which kind of defeats the purpose of using Docker in that case. I don’t really know how to proceed further. Can anyone give me some help?

Here is my docker-compose.yml file:

# vim: set fenc=utf-8 ts=2 sw=0 sts=0 sr et si tw=0 fdm=marker fmr={{{,}}}:

version: "3.7"

# {{{ Services
services:
  # {{{ Reverse proxy
  # {{{ NGINX proxy
  proxy:
    container_name: "webserver_proxy"
    restart: always
    tty: true
    build: ./build/proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - proxy-conf:/etc/nginx/conf.d
      - proxy-vhost:/etc/nginx/vhost.d
      - proxy-html:/usr/share/nginx/html
      - proxy-certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    environment:
      - DEFAULT_HOST=domain.tld
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
    networks:
      frontend:
  # }}}

  # {{{ ACME companion
  proxy-acme:
    container_name: "webserver_proxy-acme"
    restart: always
    tty: true
    depends_on:
      - proxy
    image: nginxproxy/acme-companion
    volumes:
      - proxy-conf:/etc/nginx/conf.d
      - proxy-vhost:/etc/nginx/vhost.d
      - proxy-html:/usr/share/nginx/html
      - proxy-certs:/etc/nginx/certs:rw
      - proxy-acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - DEFAULT_EMAIL=email@mail.tld
      - NGINX_PROXY_CONTAINER=webserver_proxy
    networks:
      proxy-backend:
  # }}}
  # }}}

  # {{{ To be proxied
  # {{{ Mainpage
  mainpage:
    container_name: webserver_mainpage
    restart: always
    tty: true
    build: ./build/mainpage
    expose:
      - "80"
    environment:
      - VIRTUAL_HOST=domain.tld
      - VIRTUAL_PATH=/
      - VIRTUAL_PORT=80
      - LETSENCRYPT_HOST=domain.tld
      - LETSENCRYPT_EMAIL=email@mail.tld
    networks:
      frontend:
  # }}}

  # {{{ Nextcloud Web
  nextcloud-web:
    container_name: webserver_nextcloud-web
    restart: always
    tty: true
    build: ./build/nextcloud-web
    ports:
      - "8080:80"
    expose:
      - "80"
    volumes:
      - nextcloud:/var/www/html:ro
    environment:
      - VIRTUAL_HOST=domain.tld
      - VIRTUAL_PATH=/nextcloud
      - VIRTUAL_PORT=80
      - LETSENCRYPT_HOST=domain.tld
      - LETSENCRYPT_EMAIL=email@mail.tld
    depends_on:
      - nextcloud
    networks:
      - frontend
      - nextcloud-backend
  # }}}
  # }}}

  # {{{ Nextcloud backend
  # {{{ Nextcloud
  nextcloud:
    container_name: "webserver_nextcloud"
    restart: always
    tty: true
    image: nextcloud:fpm-alpine
    depends_on:
      - nextcloud-db
      - nextcloud-cache
    volumes:
      - nextcloud:/var/www/html
    environment:
      - MYSQL_HOST=nextcloud-db
      - REDIS_HOST=nextcloud-cache
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      #- OVERWRITEWEBROOT=/nextcloud
    env_file:
      - ./env/db.env
    networks:
      nextcloud-backend:
  # }}}

  # {{{ MariaDB
  nextcloud-db:
    container_name: "webserver_nextcloud-db"
    restart: always
    tty: true
    image: mariadb:10.5
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    volumes:
      - nextcloud-db:/var/lib/mysql
    environment:
      - MYSQL_USER=nextcloud
      - MYSQL_DATABASE=nextcloud
    env_file:
      - ./env/db.env
    networks:
      nextcloud-backend:
  # }}}

  # {{{ Redis
  nextcloud-cache:
    container_name: "webserver_nextcloud-cache"
    restart: always
    tty: true
    image: redis:alpine
    networks:
      nextcloud-backend:
  # }}}

  # {{{ Cron
  nextcloud-cron:
    container_name: "webserver_nextcloud-cron"
    restart: always
    tty: true
    image: nextcloud:fpm-alpine
    depends_on:
      - nextcloud-db
      - nextcloud-cache
    entrypoint: /cron.sh
    volumes:
      - nextcloud:/var/www/html
    networks:
      nextcloud-backend:
  # }}}
  # }}}
# }}}

# {{{ Volumes
volumes:
  # {{{ For proxy
  proxy-conf:
  proxy-vhost:
  proxy-html:
  proxy-certs:
  proxy-acme:
  # }}}

  # {{{ For Nextcloud
  nextcloud:
  nextcloud-db:
  # }}}
# }}}

# {{{ Networks
networks:
  frontend:
    driver: bridge
  proxy-backend:
    driver: bridge
  nextcloud-backend:
    driver: bridge
# }}}
1 Like

Wow I have been doing exactly the same thing for the last few days. I don’t have a solution yet as I am figuring this out myself atm but I have talked to a colleague at work who suggested how it might be moved.
Given the example that we both used you keep the web nginx as it is in webroot and all you need to change is the proxy nginx config.

At the very end of /etc/nginx/conf.d/default.conf (in the proxy container) there is this block

	location / {
		proxy_pass http://yourdomain.com;
}

It might me that changing that changing to location /hextcloud is all that it takes. I have not tried that myself yet so don’t know if this is indeed the case but decided to share in case you wanna give it a try before I get the chance to.
One thing to note is that from the looks of it the nginxproxy image uses a template to build this nginx config so I am not entirely sure yet on what the proper way to change it is. The image page on docker hub has a lot of documentation but I haven’t got the chance to review it yet. I did see there was a section on changing the config.

I will update if I get it to work when I have the time to try this out.

I didn’t go into nginx-proxy’s config files, and I don’t really want to either, because they’re generated automatically and they might change (?) and because if you need to manually modify some config files, you kinda kill Docker’s purpose of self-automated containers. I wished there was something that could be done either inside the docker-compose.yml file, or even in a custom Dockerfile based on nginx-proxy.

But, if you give the web container the VIRTUAL_PATH=/nextcloud env var, doesn’t the proxy do that automatically?

Ok so I have finally figured it out.
As you said changing nginx-proxy was not the way to go. Here is what I did with my existing installation based on the same example as you:

  1. web/nginx.conf
    change the nginx.conf for nextcloud as per subdir example here (just adding /nextcloud) to a bunch of places
    NGINX configuration — Nextcloud latest Administration Manual latest documentation

  2. docker-compose.yml
    2.a. change the mounted volume from /var/www/html to /var/www/nextcloud for app, web and cron

2.b add OVERWRITEWEBROOT and NEXTCLOUD_DATA_DIR environment variables to the app
I was just playing with these and not really sure if they do much but I just left them in after I sorted the remaining issues.
Here is how my app container looks with the above changes

  app:
    image: nextcloud:fpm-alpine
    restart: always
    volumes:
      - /home/nextcloud:/var/www/nextcloud
    environment:
      - MYSQL_HOST=db
      - REDIS_HOST=redis
      - OVERWRITEWEBROOT=/nextcloud
      - NEXTCLOUD_DATA_DIR=/var/www/nextcloud/data
    env_file:
      - db.env
    depends_on:
      - db
      - redis
  1. As I already had a working installation I needed to update the php config manually on my server. in my case /home/nextcloud/config/config.php needed to have 3 paths updated from /var/www/html/.. to /var/www/nextcloud/.. (data dir will be the most important here)

I have rebuild the images and started docker-compose and now I have my nextcloud running on https://mydomain/nextcloud
Think this is all I’ve done. Have not touched VIRTUAL_PATHs at all. Ow and I have noticed that I get 502 error for a few minutes after starting everything when going to the full url so just give it a few min and it should load

the only thing to mention is that cron doesn’t seem to work now. I have added the same env vars there but have not looked further yet. If this helps and you figure out cron please let me know

UPDATE:
I fixed cron by having a custom Docker file for the container which I find super ugly but it works:

FROM nextcloud:fpm-alpine
RUN echo '*/5 * * * * php -f /var/www/nextcloud/cron.php' > /var/spool/cron/crontabs/www-data

wondering if there is a better way to change the rootdir for it than this

UPDATE 2:
So I have noticed that I was not really able to add a second service to run so I had to add VIRTUAL_PATH=/nextcloud to the web component. This made nextcloud start complaining that my .well-known/caldav and .well-known/carddav are not set up correctly on the server. I have removed the = from these 2 lines on the web nginx container:

     location /.well-known/carddav { return 301 /nextcloud/remote.php/dav/; }
     location /.well-known/caldav  { return 301 /nextcloud/remote.php/dav/; }

This didn’t stop the app from complaining but it seems to still work fine. My phone still syncs calendars via DAVx5 as before. I tried playing a bit more with the config but could not make the warning go away. Guess I’ll just ignore them

UPDATE 3:
This is not ideal - turns out parts of the app still point to /var/www/http rather that /var/www/nextcloud which I am pretty sure causes the webdav errors. I noticed this with occ not working and suggesting the app isn’t installed. Apparently I only managed to get it to work as I modified an existing installation as a blank new one results in a 404

@Andy3153 In case you haven’t fully sorted your setup by now (or anyone else stumbling on this), I have finally got around to finishing my config for this.

Took quite to figure and set everything up. Ended up amending several docker files but I am quite happy with how it now runs on in a subdir with reverse proxy and I will be able to add other services to other subdirs on the same domain in the future.

You can find full setup here: