Proxying Zotonic with nginx

It is possible to put Zotonic behind the nginx <http://nginx.org/> web server, for example if you have other, non-Zotonic virtual hosts running on your system.

When proxying, don’t forget to check the config files of the sites you are planning to server (the apps_user/yoursite/priv/zotonic_site.config files). The hostname value should not contain any port number, if you run from port 80/443: {hostname, "test.zotonic.com"}.

Zotonic configuration

Example of zotonic.config (find the location using bin/zotonic configfiles) ip/port settings when terminating SSL in nginx proxy and using plain HTTP towards the Zotonic backend:

%%% IP address on which Zotonic will listen for HTTP requests.
{listen_ip, any},

%%% Port on which Zotonic will listen for HTTP requests.
{listen_port, 8000},

%%% Port on which Zotonic will listen for HTTPS requests.
%%% Set to the atom 'none' to disable SSL
{ssl_listen_port, none},

%%% Outside port on which Zotonic will listen for HTTP requests.
{port, 80},

%%% Outside port zotonic uses to receive incoming HTTPS requests.
{ssl_port, 443},

Nginx configuration

Below is an example configuration file to proxy nginx to zotonic. Be sure to replace all occurrences of test.zotonic.com with your own hostname:

server {
      listen 80;
      listen   [::]:80 default_server ipv6only=on; ## listen for ipv6

      listen 443 ssl http2;
      listen [::]:443 ssl http2 ipv6only=on;

      server_name  test.zotonic.com;

      access_log  /var/log/nginx/test.zotonic.com.access.log;
      error_log  /var/log/nginx/test.zotonic.com.error.log;

      keepalive_timeout 65;
      gzip off;

      ssl_protocols TLSv1.2 TLSv1.3;
      ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
      ssl_prefer_server_ciphers on;

      # Disable preloading HSTS for now. Enable when you know that your
      # server certs works. You can use the header line that includes
      # the "preload" directive if you understand the implications.
      # add_header Strict-Transport-Security "max-age=15768000; includeSubdomains"; # six months
      # add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload";

      ssl_session_cache shared:SSL:10m;
      ssl_session_timeout 5m;

      # create with: openssl dhparam -out /etc/nginx/dhparam.pem 2048
      # ssl_dhparam /etc/nginx/dhparam.pem;

      ssl_certificate /path/to/ssl.crt;
      ssl_certificate_key /path/to/ssl.key;

      location / {
          proxy_pass http://127.0.0.1:8000/;
          proxy_redirect off;

          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_pass_request_headers on;

          client_max_body_size       0;
          client_body_buffer_size    128k;

          proxy_connect_timeout      90;
          proxy_send_timeout         90;
          proxy_read_timeout         90;

          proxy_buffer_size          4k;
          proxy_buffers              4 32k;
          proxy_busy_buffers_size    64k;
          proxy_temp_file_write_size 64k;
      }

      location /mqtt-transport {
          proxy_pass http://127.0.0.1:8000/mqtt-transport;

          proxy_http_version 1.1;

          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_set_header Upgrade           $http_upgrade;
          proxy_set_header Connection        "upgrade";

          proxy_pass_request_headers on;
      }

      location /close-connection {
           keepalive_timeout 0;
           empty_gif;
      }
}

As shown in the example above:

  • Add X-Forwarded-Proto to proxied header so that Zotonic knows that HTTPS is used before the proxy even though HTTP is used between the proxy and backend.
  • Add X-Real-IP and X-Forwarded-For headers.

Zotonic always redirects to HTTPS so the proxy needs to be configured for both HTTP and HTTPS.

Zotonic makes use of a websocket connection for MQTT messages at the /mqtt-transport endpoint, so you need to pass the Upgrade and Connection headers.

The /mqtt-transport endpoint is also used to POST uploaded files using a HTML multi-part form post.

See the nginx documentation for more information on its configuration procedure.