NGINX Unit is a fully dynamic application server that can serve multiple languages as well as multiple versions of each language. It’s dynamic in the sense that you use the RESTful JSON API to make changes to its configuration in memory, without service disruption or configuration reloads.
In my presentation at NGINX Conf 2018 in October, I showed how to configure a new application in an existing production environment. Specifically, with WordPress running on PHP, I deployed a Python application that uses the Django framework. I also showed show how you can load configuration both from a file and as specified with an argument to an API call.
This blog includes all of the commands and configuration code I used in the demo, to make it easier for you to adapt to your own deployment.
Prerequisites
For the demo at NGINX Conf, I had the following software installed:
- Ubuntu 16.04
- NGINX Plus, but you can use NGINX Open Source except as noted
- NGINX Unit with all language modules installed
- Python 3
- Django (not configured – that’s what the demo and this blog are about)
root
privilege, or equivalent access viasudo
(which we use where necessary)
I also had PHP and WordPress installed as the existing application.
Creating the Django Project
-
Change into the directory where we’re creating our Django project:
$ cd /var/www/
-
Use the django-admin
startproject
command to initialize the new project. We’re calling it djapp.
$ sudo django-admin startproject djapp
-
Change into the project directory:
$ cd djapp
-
Use the manage.py
script to migrate the database for the project, which is necessary for a newly created project. Django uses SQLite by default, and I accept the default in the demo, but you can use any database that meets your project’s needs.
The manage.py
script is installed by the django-admin
command we ran in Step 2; it performs the same commands and accepts the same arguments as django-admin
, but automatically derives and uses some project‑specific settings, which is helpful. For details, see the Django documentation.
$ sudo python3 manage.py migrate
-
Although it’s not strictly necessary for a sample project like this one, we recommend that you create a Django superuser identity:
$ sudo python3 manage.py createsuperuser
-
Change into the subdirectory that contains the settings.py file, which was created by the django-admin
startproject
command in Step 2.
$ cd /var/www/djapp/djapp
-
Using your preferred text editor, open settings.py. Here we’re using nano
:
$ sudo nano settings.py
Find the ALLOWED_HOSTS
line and add in the domain name, hostname, or IP address for the application:
ALLOWED_HOSTS = ['domain-name']
Also add the following line at the end of the file, to name the directory that stores all static content served by the application (see Step 9).
STATIC_ROOT = '/var/www/djapp/djapp/static'
-
Change back to the main project directory (where manage.py resides).
$ cd ..
-
Run the manage.py
collectstatic
command to collect all static files located in the Django project and put them into the STATIC_ROOT
location defined in Step 7.
$ sudo python3 manage.py collectstatic
Configuring NGINX
Change into the directory where we’re creating our Django project:
$ cd /var/www/
Use the django-admin
startproject
command to initialize the new project. We’re calling it djapp.
$ sudo django-admin startproject djapp
Change into the project directory:
$ cd djapp
Use the manage.py
script to migrate the database for the project, which is necessary for a newly created project. Django uses SQLite by default, and I accept the default in the demo, but you can use any database that meets your project’s needs.
The manage.py
script is installed by the django-admin
command we ran in Step 2; it performs the same commands and accepts the same arguments as django-admin
, but automatically derives and uses some project‑specific settings, which is helpful. For details, see the Django documentation.
$ sudo python3 manage.py migrate
Although it’s not strictly necessary for a sample project like this one, we recommend that you create a Django superuser identity:
$ sudo python3 manage.py createsuperuser
Change into the subdirectory that contains the settings.py file, which was created by the django-admin
startproject
command in Step 2.
$ cd /var/www/djapp/djapp
Using your preferred text editor, open settings.py. Here we’re using nano
:
$ sudo nano settings.py
Find the ALLOWED_HOSTS
line and add in the domain name, hostname, or IP address for the application:
ALLOWED_HOSTS = ['domain-name']
Also add the following line at the end of the file, to name the directory that stores all static content served by the application (see Step 9).
STATIC_ROOT = '/var/www/djapp/djapp/static'
Change back to the main project directory (where manage.py resides).
$ cd ..
Run the manage.py
collectstatic
command to collect all static files located in the Django project and put them into the STATIC_ROOT
location defined in Step 7.
$ sudo python3 manage.py collectstatic
By default, Django itself serves the static content for a project, but NGINX Open Source and NGINX Plus offer superior performance. Here we configure NGINX Plus, but you can use NGINX Open Source except for one feature noted below.
-
Change directory to /etc/nginx/conf.d, the conventional location for function‑specific (or in our case, application‑specific) HTTP configuration files:
$ cd /etc/nginx/conf.d
-
Create a file called django.conf (again, we’re using
nano
):$ sudo nano django.conf
Insert the following configuration, which enables caching.
The configuration also includes two features that are exclusive to NGINX Plus. Uncomment the relevant directives if you are using NGINX Plus and want to take advantage of the features:
- Extended metrics collection, enabled for this virtual server by the
status_zone
directive. I’m assuming that the NGINX Plus API is enabled elsewhere in the configuration. - Active health checks, enabled by the
health_check
directive.
One thing to note is that in the demo at NGINX Conf I specified the IP address of my local machine as the second argument to the
proxy_set_header
directive. In a production environment, it makes more sense to use the$host
variable as shown below.# Upstream group for the backend (NGINX Unit running the Python application) upstream django_unit { zone django_unit 64k; server 127.0.0.1:8000; } server { listen 8080; # Uncomment to collect metrics if using NGINX Plus and the NGINX Plus API #status_zone django; # enable caching proxy_cache django_cache; proxy_cache_valid 200 60m; # root directory for static files root /var/www/djapp/djapp; # proxy to the NGINX Unit backend location / { proxy_pass http://django_unit; # Second argument must match your production hostname and the value of # ALLOWED_HOSTS in settings.py proxy_set_header Host $host; # Uncomment to enable active health checks if using NGINX Plus #health_check; } # Location for the static files collected from Django and served by # NGINX Plus; can be empty (as here), because it inherits the value of the # 'root' directive from its parent block location /static { } }
- Extended metrics collection, enabled for this virtual server by the
-
Check the configuration for syntactic validity:
$ sudo nginx –t
-
After fixing any errors, reload the configuration:
$ sudo nginx -s reload
Configuring NGINX Unit
To finish up, we need to configure NGINX Unit to serve the requests to the application.
-
Run this
curl
command to display the current NGINX Unit configuration, which is for WordPress running on PHP. I don’t show the output here, but the WordPress configuration appears in Step 6 below, along with the Python application’s configuration, which we’re about to add.Note that I use
sudo
for thecurl
command, which you may not need to do for mostcurl
commands. Here it’s necessary because to access the UNIX socket we need the read‑write permission thatroot
has on it.$ sudo curl --unix-socket /run/control.unit.sock http://localhost/config/
-
Change to the directory for NGINX Unit configuration files.
Keep in mind that these files are optional and just a convenient way to load collections of configuration without typing all the data as an argument to a call to the NGINX Unit API. Because the content of the files is uploaded through the API (like all configuration data), NGINX Unit does not know about file locations and cannot automatically read files as it starts (unlike NGINX Open Source and NGINX Plus). Instead, NGINX Unit saves its runtime state in a separate directory.
$ cd /etc/unit
-
Create a file called django.config (again, we’re using
nano
):$ sudo nano django.config
Add the following JSON, which represents our Python application.
{ "type": "python", "processes": 5, "module": "djapp.wsgi", "path": "/var/www/djapp" }
-
Run this
curl
command to load the JSON contained in django.config as a new application object to be managed by NGINX Unit, called djapp:$ sudo curl -X PUT --data-binary @/etc/unit/django.config --unix-socket /run/control.unit.sock http://localhost/config/applications/djapp
In this command:
- The HTTP
PUT
method creates a new NGINX Unit configuration object at the location named by the final argument (the URL). See the final bullet below. - The
--data-binary
argument tellscurl
to load the contents of django.config exactly as provided, preserving newlines and carriage returns, and not doing processing of any kind. - The
--unix-socket
argument defines where the NGINX Unit API is listening. (We use thesudo
command because we’re using the default owner of the socket,root
.) - The final argument locates and names the new application object to populate with the JSON‑formatted configuration data in django.config:
config
is the top‑level NGINX Unit configuration object,applications
the parent for application objects, anddjapp
the name of the new application object.
- The HTTP
-
Define the listener object for the application. Rather than loading a file of configuration data as in Step 4, we define the data directly on the
curl
command line, specifying that thedjapp
application listens on port 8000.$ sudo curl -X PUT --data-binary '{"application":"djapp"}' --unix-socket /run/control.unit.sock 'http://localhost/config/listeners/*:8000'
-
Repeat the
curl
command from Step 1 to display the NGINX Unit configuration, which now includes our Python application, djapp, highlighted in orange:$ sudo curl --unix-socket /run/control.unit.sock http://localhost/config/ { "listeners": { "127.0.0.1:8090": { "application": "script_index_php" }, "127.0.0.1:8091": { "application": "direct_php" }, "*:8000": { "application": "djapp" } }, "applications": { "script_index_php": { "type": "php", "processes": { "max": 20, "spare": 5 }, "user": "www-data", "group": "www-data", "root": "/var/www/wordpress", "script": "index.php" }, "direct_php": { "type": "php", "processes": { "max": 5, "spare": 0 }, "user": "www-data", "group": "www-data", "root": "/var/www/wordpress", "index": "index.php" }, "djapp": { "type": "python", "processes": 5, "module": "djapp.wsgi", "path": "/var/www/djapp" } } }
Summary
In this post we started with NGINX Unit running PHP applications for WordPress in production, and added a Python application. In the demo, I use the NGINX Plus dashboard to show that there is no disruption to the existing applications when a new application is added, but you can use any system‑monitoring tool, such as the ps
command, for that purpose. The dynamic nature of NGINX Unit configuration saves resources for your running applications, and ensures zero downtime on new deployments and smooth transition between application versions.
To learn more, visit unit.nginx.org.