Menu

Setting up NGINX Unit (and switching from uWSGI)


For quite a few years our Django/Wagtail apps have been deployed to production on NGINX/uWSGI running on Ubuntu. With several servers in need of being upgraded, and with uWSGI now in maintenance mode, we decided to try out NGINX Unit. After some research to figure out how to manually configure a few things on the server (documented below), we were able to successfully make the switch from uWSGI to NGINX Unit.

First, in Django

If a project does not have CSRF_TRUSTED_ORIGINS set, then, even though it runs fine on uWSGI, secure submissions of login/logout forms or anything that includes a CSRF_TOKEN will likely fail when deployed on NGINX Unit. uWSGI automatically sends to Django the information it needs to be able to determine whether a request is made securely. To fix this problem without having to set CSRF_TRUSTED_ORIGINS (which would require more specificity), just place the following line in the settings file:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

This line uses the header information that is passed via the NGINX setting X-Forwarded-Proto mentioned below. More information about SECURE_PROXY_SSL_HEADER is here.

NGINX Unit Language Plugins

NGINX Unit has pluggable language support via Ubuntu packages. This design means that you must apt install the package for the specific language/version package that you wish to use. You might, however, find that a specific language/version package is not available for a particular Ubuntu distribution. Using Python as the example language, if you try to install unit-python3.8 or unit-python3.9 on Ubuntu 22.04, you will see the message:
Couldn't find any package by glob 'unit-python3.9'

Interestingly, though, you are able to install unit-python2.7 on Ubuntu 22.04.

The pluggable language design also means that a single Unit installation can serve sites that run on multiple languages. So, for example, if a client has a legacy WooCommerce site but they want their main informational WordPress site to be converted to Wagtail, then you would just need to set up NGINX Unit appropriately to run both the legacy WooCommerce site and the new Wagtail-based site under the same domain.

How does NGINX work with NGINX Unit?

In addition to serving dynamic pages as does uWSGI, NGINX Unit can be configured to serve static files and so could replace the open-source version of NGINX in a lot of cases. However, we use LetsEncrypt certificates, and NGINX Unit does not (yet) support obtaining LetsEncrypt certificates in the same easy way that NGINX does. Therefore, it was decided to keep NGINX in place to serve static and media files and to just use NGINX Unit in the same way that uWSGI was being used - to serve dynamic web pages.

Installing NGINX Unit

First do the preliminary setup and installation, including installing the appropriate language package based on the Ubuntu version. For example, on Ubuntu 20.04, since the system Python version is 3.8, you would do:
sudo apt install unit unit-python3.8

Do systemctl enable unit to ensure unit runs when the server restarts.

Manually prepare items on the server after installing Unit

  • Media folder permissions in Django projects: Both NGINX and uWSGI run as the www-data user, so ownership on the media folder had been set to www-data:www-data. Unit, however, runs as the unit user. To ensure Unit can manage the uploading of files, the media folder ownership was changed to unit:www-data by doing: chown -R unit:www-data /path/to/site/media
  • The standard location for the socket file that provides for communication between Unit and NGINX would be /run/unit/. Therefore, ensure the /run/unit/ folder is automatically created with the correct ownership (unit:www-data) by creating the file /etc/tmpfiles.d/unit.conf with the following in it: d /run/unit 0755 unit www-data -
    (After creating the unit.conf file, do systemd-tmpfiles --create to apply the contents of /etc/tmpfiles.d/unit.conf and create the /run/unit/ folder immediately without having to restart the server.)
  • In order to deal with a permissions error, you might have to add the following line to the Unit service file (/lib/systemd/system/unit.service):
    ExecStartPost=/bin/chown unit:unit /run/unit.pid
  • Do systemctl restart unit and systemctl status unit to verify that Unit is running OK.

Setting up the Unit configuration file

According to the docs, "Unit’s configuration is JSON-based, accessible via a RESTful control API, and entirely manageable over HTTP". Insertion of the configuration into the running Unit instance happens via a PUT. Note that the Unit /config/ url does not support PATCH, so if you have a lot of websites on a single server, you will not be able to create a separate configuration file for each site in the same way that individual vassal files might be created when running uWSGI in Emperor mode. Unit requires a single JSON-formatted configuration, and we follow the convention of storing that JSON in a file called config.json.

We recommend following the Linux convention of having a directory for a service be located in /etc/. Therefore, create the /etc/unit/ directory. In the case of NGINX or uWSGI, there would likely also be subdirectories under the main service-related directory, but there is no need for subdirectories under /etc/unit/. In the /etc/unit/ directory we recommend placing the following two items:

  • A config.json file. This file will hold the JSON-based configuration for Unit.
  • A put_config file with the following content:
    echo
    curl -X PUT --data-binary @/etc/unit/config.json --unix-socket /var/run/control.unit.sock http://localhost/config/
    echo

put_config is a convenience file for easily executing the curl command. After creating the put_config file, do chmod 700 /etc/unit/put_config to make the file executable.

The following is a skeleton outline for the contents of a config.json file which serves to handle multiple sites:

{
  "listeners": {
    "unix:/run/unit/site1.socket": { "pass": "routes/site1" },
    "unix:/run/unit/site2.socket": { "pass": "routes/site2" },
  },
  "routes": {
    "site1": [
               {
                 "action": { "pass": "applications/site1" }
               }
             ],
    "site2": [
               {
                 "action": { "pass": "applications/site2" }
               }
             ]
  },
  "applications": {
    "site1": {
      "type": "python 3.8",
      "path": "/path/to/site1",
      "home": "/path/to/virtual_env_site1",
      "module": "site1.wsgi",
      "working_directory": "/var/sites/site1"
    },
    "site2": {
      "type": "python 3.8",
      "path": "/var/sites/site2",
      "home": "/path/to/virtual_env_site2",
      "module": "site2.wsgi",
      "working_directory": "/var/sites/site2"
    }
  }
}

After you have the configuration for at least one site set up in config.json, use the jq utility to validate the structure of the json by doing:

jq . /etc/unit/config.json

If the json validates, then execute the following to load the configuration into Unit:

/etc/unit/./put_config

If your configuration is set up correctly, you will see:

{
  "success": "Reconfiguration done."
}

Troubleshooting

If the json in config.json validates but Unit will still not accept the configuration, look for a socket file in /run/unit/ that corresponds to the site that you are trying to configure. If a socket file exists, delete it and run /etc/unit/./put_config again.

Config.json Successfully Loaded

If your site(s) are currently running under NGINX/(uWSGI/gunicorn/?), the site(s) should still be running fine at this point. Now, verify that the socket file is in /run/unit/ and, if it is, you are ready to finish the setup by making the necessary NGINX server block changes.

Changes to NGINX server block files

In the location / { ... } block for each site, replace uwsgi_pass and any related lines with the following:

proxy_pass http://unix:/run/unit/__________.socket;
include /etc/nginx/include/proxy_pass_set_headers.include;

In the above, note the use of http:// before unix:/. The http:// prefix is not necessary for uwsgi_pass, but it is necessary for proxy_pass. Also, be sure to replace ____________.socket with the actual name of the socket file for the app. Then, make sure the /etc/nginx/include/ directory is present, and then create the file /etc/nginx/include/proxy_pass_set_headers.include with the following content:

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;

Now do nginx -t to ensure the configuration is set up correctly. If the configuration is correct, then do systemctl reload nginx. The dynamic portion of your site is now being served by Unit.

Final Comments

NGINX Unit does not come with a reload command; in addition to start and stop, there is only a restart command. There is also no command to test the configuration as with NGINX's nginx -t. When a PUT of the configuration is attempted, it is either accepted or rejected by Unit. If the configuration is accepted, then it is immediately deployed without having to restart the Unit service.

In uWSGI Emperor mode, the Emperor monitors .ini vassal files and dynamically spawns, reloads, or destroys vassals when files are created, modified, or deleted, without restarting the Emperor. Modified vassals are reloaded, which may briefly interrupt their service. uWSGI validates .ini files, but errors in configuration can prevent a vassal from starting or reloading, potentially causing downtime for that application. In contrast, changes to the Unit configuration have to be applied manually, and the configuration is rejected if it is not valid. We have found that this method of accepting configuration means less likelihood of downtime with Unit because of a bad configuration.

Finally, according to the NGINX Unit documentation, it can serve both WSGI and ASGI apps. So far, we have only deployed ASGI apps (that use Django Channels) on Daphne. NGINX Unit appears to be a good alternative should we ever decide to switch from deploying on Daphne.

Share: