Seeking guidance getting simple Volto-frontend install working behind nginx TLS/SSL

I am trying to do something relatively simple with Plone 6 using the Volto frontend. I want to migrate my personal website, currently running on old Plone installation (4.2!), and like where Volto is heading. I don’t necessarily need full developer capabilities, but do need to run it behind TLS/SSL and be able to add and change addons. At this point I am unable to tell whether the instructions for installing Plone 6 using containers (for instance the nginx, Frontend, Backend container example) is sufficient to deploy behind nginx configured for TLS/SSL.

Specifically, I see that the Frontend section of the Plone 6 dev docs has its own Deploying section, including Simple deployment – Frontend – Deploying which has suggestions about nginx TLS configuration, but I can’t tell how to implement those suggestions.

Here’s what I have working:

  • I have the frontend + backend access working via port 80 and via direct access to port 3000, using the nginx, Frontend, Backend container example

  • I have an nginx + TLS/SSL configuration working with static content

  • When I try to access the frontend behind nginx TLS I am blocked by CORS mixed content restrictions. In the browser console:

    Mixed Content: The page at '<URL>' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '<URL>'. This request has been blocked; the content must be served over HTTPS
    

I would like to implement the Frontend deploying — Plone Documentation v6.0-dev instructions, so the RAZZLE_API_PATH=https://mywebsite.com/api , but don’t see how to do that in the nginx, Frontend, Backend container example that I'm using. Suggestions appreciated!

Ken

I think that what you are missing is that if you have SSL, you have to amend the rewrite accordingly like:

rewrite ^/(\+\+api\+\+\/?)+($|/.*) /VirtualHostBase/https/$server_name:443/Plone/++api++/VirtualHostRoot/$2 break;

More on Zoe's VHM rewrites: 20. Virtual Hosting Services — Zope documentation 5.7 documentation

I recommend that you use "Seamless mode":

Also, take a look at the cookiecutter template, where there is a lot of knowledge poured on it:

I hope this helps!

I don't think that addresses the problem. I believe the CORS mixed content error is due to javascript requests on the https page that the browser received trying to reach non-https ports, specifically 8080. Here's one of the links that are failing, as revealed by the browser dev-tools network report:

http://do.myriadicity.net:8080/Plone/++api++/@navigation?expand.navigation.depth=1

Changing the rewrite rule you mentioned doesn't help with that – I still get the request for 8080 on the resulting page.

I guess what I need to find out is how to change the address that the frontend requests are making use a particular path rather than a port, and then translate that path (with the rewrite rule) to reach out to the port.

Looking through the plone/plone-frontend list of configuration variables, it looks like the RAZZLE_INTERNAL_API_PATH environment variable is the one I want.

Yes! After rebuilding the containers with RAZZLE_INTERNAL_API_PATH set to a path like http://backend/something_distinct/... I no longer get the mixed content error! I think I found what I'm looking for. Now I will need to adjust the rewrite rule to unmangle that and proxy_pass the request to the backend.

I'll look at the references you sent to see if they provide insights about doing that. Thanks for taking the time to help!

Ken

[In a subsequent message I posted a new version of the configuration described in this message that solves the problem described here.]

Ok! I’ve got a Plone 6.0.0rc1 site fully operational behind https/TLS, though there’s one minor glitch left with which I could use help. I’m including below a variation of the configuration I’m using, based on the nginx, Frontend, Backend container example, because others might find it useful (I would have!), plus it will make it easy for those in the know to guide me in addressing the remaining problem.

The problem is a momentary “Connection Refused” message in the content area of pages when I visit them using an external link. By “momentary” I mean that the proper content replaces the “Connection Refused” message almost immediately after that message is displayed. This does not otherwise occur, eg when navigating from page to page using links on the site.

You can currently see the behavior by visiting https://do.myriadicity.net.

I expect this is related to Volto’s two-stage page loading process. Unfortunately, I am not clear about the specifics of that process nor found a concise explanation and haven’t made any headway trying debug my configuration. I suspect that someone familiar with the process will quickly recognize the cause. If you do, I would appreciate it very much if you would steer me in the right direction!

Here’s my setup. It’s directly derived from nginx, Frontend, Backend container example, with the addition of LetsEncrypt certs established in the host file system and brought to the docker-composed nginx instance using data volumes. I also use data volumes for the backend storage for easy external access, for persistence and migration.

  1. Here’s my adapted docker-compose.yml file. The crucial difference is the setting for RAZZLE_INTERNAL_API_PATH. I suspect that the problem I mention above is because I haven’t got this setting quite right, but as I said everything else does work. (I also changed the backend version to Plone 6.0.0rc1. Docker compose makes it extremely easy to update stuff!)

    version: "3"
    services:
    
      webserver:
        image: nginx
        volumes:
          - ./default.conf:/etc/nginx/conf.d/default.conf
          - /etc/letsencrypt:/etc/letsencrypt
        depends_on:
          - backend
          - frontend
        ports:
          - "80:80"
          - "443:443"
    
      frontend:
        image: plone/plone-frontend:latest
        environment:
          RAZZLE_INTERNAL_API_PATH: http://backend/Plone
          CORS_ALLOW_ORIGIN: "*"
          CORS_ALLOW_CREDENTIALS: "true"
        ports:
          - "3000:3000"
        depends_on:
          - backend
    
      backend:
        image: plone/plone-backend:6.0.0rc1
        environment:
          SITE: Plone
        volumes:
          - /var/local/data/trial/filestorage:/data/filestorage
          - /var/local/data/trial/blobstorage:/data/blobstorage
        ports:
          - "8080:8080"
    
    volumes:
      data: {}
    
  2. Here’s my adapted nginx default.conf, implementing ssl and forwarding port 80 access to ssl. It uses www.example.com for the website hostname.

    upstream backend {
      server backend:8080;
    }
    upstream frontend {
      server frontend:3000;
    }
    
    server {
      server_name www.example.com;
    
      listen 443 ssl; # managed by Certbot
      ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; # managed by Certbot
      ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; # managed by Certbot
      include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
      ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    
      location ~ /\+\+api\+\+($|/.*) {
          #error_log /home/klm/scratch/myrerrlog notice;
          rewrite ^/(\+\+api\+\+\/?)+($|/.*) /VirtualHostBase/https/$server_name/Plone/++api++/VirtualHostRoot/$2 break;
          proxy_pass http://backend;
      }
    
      location ~ / {
          location ~* \.(js|jsx|css|less|swf|eot|ttf|otf|woff|woff2)$ {
              add_header Cache-Control "public";
              expires +1y;
              proxy_pass http://frontend;
          }
          location ~* static.*\.(ico|jpg|jpeg|png|gif|svg)$ {
              add_header Cache-Control "public";
              expires +1y;
              proxy_pass http://frontend;
          }
          proxy_set_header        Host $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_redirect http:// https://;
          proxy_pass http://frontend;
      }
    }
    
    server {
    
      listen 80 default_server;
      server_name www.example.com;
      location ~ /\+\+api\+\+($|/.*) {
          rewrite ^/(\+\+api\+\+\/?)+($|/.*) /VirtualHostBase/http/$server_name/Plone/++api++/VirtualHostRoot/$2 break;
          proxy_pass http://backend;
          }
       if ($host = www.example.com) {
           return 301 https://$host$request_uri;
       } # managed by Certbot
       return 404; # managed by Certbot
    }
    

Some more possibly useful notes:

  • I’m using data volumes to share the LetsEncrypt installed and updated certs with the containerized nginx install. I will need to tweak the LetsEncrypt renewed-cert procedure to restart that containerized nginx install, using something like cd dockercompose-dir && docker compose restart webserver.

  • Alternatively, there’s a very promising repository offering a containerized packaging of the LetsEncrypt + nginx facilities: evgeniy-khist/letsencrypt-docker-compose: Nginx and Let’s Encrypt with Docker Compose in less than 3 minutes (github.com)

  • During initial docker composition of the “Plone Volto site: Plone” a banner message includes “THIS IS NOT MEANT TO BE USED IN PRODUCTION”. I would like to know more about why that is, and whether I should rethink basing my site’s install on all this. The page referenced with a link includes this warning: We advise against using this feature on production environments., (in reference to a docker run command), but isn’t particularly illuminating.

The "connection refused" message is from the Server Side Rendering. The problem is like this:

  1. Volto nodejs server wants to generate an HTML for your browser to load. It tries to connect to Plone and fetch the content, so that the generated HTML is already showing you the proper content.
  2. Now your browser will load the generated HTML and then execute the Javascript that is Volto running in your browser. That Javascript will connect to the Plone backend and it will load and redisplay the content. This process is called hydration.

So, what happens is that you briefly see the HTML produced by the server, then the client-side app loads. For some reason the server is unable to connect to the Plone backend.

Thanks for the explanation! Evidently RAZZLE_INTERNAL_API_PATH specifies how the frontend connects with the backend for that initial step that you describe. I looked back at the frontend docker run command on the Containers overview page and the value used there for RAZZLE_INTERNAL_API_PATH, http://backend:8080/Plone, was the solution. (Setting RAZZLE_API_PATH according to that comand breaks things, however.) My bare-bones Plone 6 installation, using the Volto frontend, is now fully operational behind TLS. Yay!!

I'm going to reiterate the configuration, with that setting corrected.

This configuration directly descends from nginx, Frontend, Backend container example, with the addition of LetsEncrypt certs established in the host file system and brought to the docker-composed nginx instance using data volumes. I also use data volumes for the backend storage for easy external access, for persistence and migration.

Note that the configuration depends on backend and frontend being defined as the host where the backend and frontend ports are established. I set them in my hosts file, so the localhost line in my /etc/hosts file looks like: 127.0.0.1 localhost backend frontend

  1. Here's my docker-compose.yml file:

    version: "3"
    services:
    
      webserver:
        image: nginx
        volumes:
          - ./default.conf:/etc/nginx/conf.d/default.conf
          - /etc/letsencrypt:/etc/letsencrypt
        depends_on:
          - backend
          - frontend
        ports:
          - "80:80"
          - "443:443"
    
      frontend:
        image: plone/plone-frontend:latest
        environment:
          RAZZLE_INTERNAL_API_PATH: <http://backend:8080/Plone>
          CORS_ALLOW_ORIGIN: "*"
          CORS_ALLOW_CREDENTIALS: "true"
        ports:
          - "3000:3000"
        depends_on:
          - backend
    
      backend:
        image: plone/plone-backend:6.0.0rc1
        environment:
          SITE: Plone
        volumes:
          - /var/local/data/trial/filestorage:/data/filestorage
          - /var/local/data/trial/blobstorage:/data/blobstorage
        ports:
          - "8080:8080"
    
    volumes:
      data: {}
    
    
  2. Here’s my adapted nginx default.conf, implementing ssl and forwarding port 80 access to ssl (it’s unchanged from my previous message):

    upstream backend {
      server backend:8080;
    }
    upstream frontend {
      server frontend:3000;
    }
    
    server {
      server_name www.example.com;
    
      listen 443 ssl; # managed by Certbot
      ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; # managed by Certbot
      ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; # managed by Certbot
      include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
      ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    
      location ~ /\\+\\+api\\+\\+($|/.*) {
          #error_log /home/klm/scratch/myrerrlog notice;
          rewrite ^/(\\+\\+api\\+\\+\\/?)+($|/.*) /VirtualHostBase/https/$server_name/Plone/++api++/VirtualHostRoot/$2 break;
          proxy_pass <http://backend>;
      }
    
      location ~ / {
          location ~* \\.(js|jsx|css|less|swf|eot|ttf|otf|woff|woff2)$ {
              add_header Cache-Control "public";
              expires +1y;
              proxy_pass <http://frontend>;
          }
          location ~* static.*\\.(ico|jpg|jpeg|png|gif|svg)$ {
              add_header Cache-Control "public";
              expires +1y;
              proxy_pass <http://frontend>;
          }
          proxy_set_header        Host $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_redirect http:// https://;
          proxy_pass <http://frontend>;
      }
    }
    
    server {
    
      listen 80 default_server;
      server_name www.example.com;
      location ~ /\\+\\+api\\+\\+($|/.*) {
          rewrite ^/(\\+\\+api\\+\\+\\/?)+($|/.*) /VirtualHostBase/http/$server_name/Plone/++api++/VirtualHostRoot/$2 break;
          proxy_pass <http://backend>;
          }
       if ($host = www.example.com) {
           return 301 https://$host$request_uri;
       } # managed by Certbot
       return 404; # managed by Certbot
    }
    
    

Some further details worth noting:

  • I’m using data volumes to share the LetsEncrypt installed and updated certs with the containerized nginx install. I will need to tweak the LetsEncrypt renewed-cert procedure to restart that containerized nginx install, using something like cd dockercompose-dir && docker compose restart webserver.
  • Alternatively, there’s a very promising repository offering a containerized packaging of the LetsEncrypt + nginx facilities: evgeniy-khist/letsencrypt-docker-compose: Nginx and Let’s Encrypt with Docker Compose in less than 3 minutes (github.com)
  • During initial docker composition of the “Plone Volto site: Plone” a banner message includes “THIS IS NOT MEANT TO BE USED IN PRODUCTION”. I would like to know more about why that is, and whether I should rethink basing my site’s install on all this. The page referenced with a link includes this warning: We advise against using this feature on production environments., (in reference to a docker run command), but isn’t particularly illuminating.
2 Likes