Our customers often ask us how to use NGINX Unit in an already established technology stack of some sort. On its own, NGINX Unit is pretty easy to configure, but the proper way to inject it into a diverse set of tools and services may be less obvious; the same questions apply to the potential benefits for the end customer. This post aims to shed some light on this topic in a rather common use case.
The Problem: Complicated App Deployment Workflows
An issue that many of our readers have probably encountered is the need to automate procedures for deployment of production environments that involve custom‑built language runtime versions as well as specific libraries, modules, and extensions to suit specific business needs. Usually, this can be achieved with reasonable ad hoc effort, but the difficulty of turning a custom semi‑manual workflow into a series of adjustable automated steps suitable for a larger scale can prove so tedious that you choose to scrap the project altogether. Let’s see how NGINX Unit simplifies matters in this case.
Scenarios of this kind are usually complicated by overlapping requirements, such as:
- Automating the setup procedure for the web app itself
- Building a customized language runtime to suit the app’s needs
- Injecting deployment‑specific settings into the resulting generic image
- Deploying and managing instantiated images
Each of these requirements can be satisfied on its own, but again the sheer pressure of having to repeat all the steps over and over just to get to the interesting part of your web app can be overwhelming. Here, we suggest an approach that alleviates this pain and allows you to spend more time adding business value instead of laying down the piping. You can use it as is or adapt it to more elaborate scenarios.
The Solution: Docker and NGINX Unit
Essentially, what we describe here is an NGINX Unit-based rig for a web app of your choice, devised by our own Timo Stark and available on GitHub; kudos is welcome. The build stack has WordPress for the target app and is aptly called unitwp. However, it can be adapted to other options: for instance, it already powers a CakePHP‑based website, https://kaufdaheim.org, which is helping local businesses in Germany stay afloat during the ongoing COVID‑19 lockdown.
The setup described here almost effortlessly creates a custom NGINX Unit-based container image with built‑in PHP support, prepares a WordPress installation inside the image, primes the web application to launch in NGINX Unit, and finally runs and exposes it on a pre‑configured port. The setup deliberately avoids including a database, for reasons discussed in Setting Up Development Environments and Using Environment Variables.
The solution relies on an official NGINX Unit image that has been pre‑configured with the PHP 7.3 runtime and the appropriate language module for NGINX Unit, which saves quite a lot of time and effort compared to preparing a custom image. If you prefer a bare‑bones Docker image that you can build upon as you see fit, use the nginx/unit:1.16.0-minimal
image, which contains only the core NGINX Unit executable. For recommendations on using NGINX Unit with Docker, see our official Docker how‑to.
Note: The following step assumes that you have the Composer dependency manager for PHP; follow the download instructions if necessary. Otherwise, you can continue with any other installation method available.
We start by creating a local copy of the files for our WordPress application, employing this basic Composer configuration which uses a Composer‑enabled WordPress fork on GitHub:
To download and prepare the assets, save this snippet as composer.json
and run the composer
install
command in the same directory.
Now, let’s load the files into a Docker image using the following Dockerfile:
This file first customizes the base image’s PHP environment with a few packages required by WordPress. It’s the first potential extension point if your project requires extra packages or customizations. If you prefer to build the entire toolchain yourself, you can start with a -minimal
base image instead as noted above.
The file then copies the WordPress files to the image from the directory created by Composer and sets the appropriate ownership. Finally, it copies in the JSON snippet that will be uploaded to the NGINX Unit configuration when the container starts up. The startup script operates automatically, but you can also make use of custom certificates and shell scripts as discussed in the NGINX Unit documentation.
Here’s the NGINX Unit configuration:
The WordPress‑specific routing scheme (adopted from our WordPress how‑to) makes use of the fallback
routing option introduced in NGINX Unit 1.16.0, which is similar to NGINX’s try_files
directive. Notice also that we have applied different settings to different parts of the website in the separate wp_direct
and wp_index
objects in the applications
section; you can even use different php.ini files if you wish.
Last, we wrap this all up in a nice little build.sh
script:
For YOURIMAGETAG
substitute your local tag, and for REMOTEIMAGETAG
the final tag which includes the name of the target Docker registry. For brevity, the script omits error handling and reporting.
When we run the build.sh
push
command, the script builds a new image from scratch, tags it, and pushes it to our registry of choice. You can enhance the shell script as well, for example introducing adjustable tags and versioning, or scrapping stale versions of the same image to reduce clutter.
Setting Up Development Environments
A related and equally relevant scenario that can benefit from NGINX Unit’s capabilities even more is the need to spin up and shut down local development or testing environments quickly and easily. The dynamic configuration features of NGINX Unit make it a promising option for seamlessly switching between language runtime versions, environment variables, or just about any other settings of the app you’re working on. Let’s build upon what we’ve achieved so far.
The development version of our unitw WordPress app is found in the dev subdirectory of the GitHub repository. It uses essentially the same setup as in The Solution: Docker and NGINX Unit, with a few differences.
The first difference is in the Dockerfile, which has the following command instead of the command that copies WordPress files to the image. This change enables you to add specific php.ini settings every time you build the image.
Another notable difference is the usage of docker-compose
to add a MariaDB database container. Here’s the docker-compose.yaml
file:
It’s a Docker best practice to maintain one service per container, and that’s the reason we didn’t add a database in The Solution: Docker and NGINX Unit. In a production environment, database connectivity can come in many flavors, so we have kept our container generalized and ready for any scenario, instead of choosing a specific database. In contrast, a development environment usually needs to be self-contained, so the dynamic configuration in this section creates a fully operational stack at runtime. This involves the heavy use of environment variables, which are discussed in detail in the next section.
To run this setup, we need an upgraded shell script, named here run-local.sh
, with one option for building an image and running the containers, and another for stopping services and performing cleanup:
To start and stop a local development environment, run the ./run-local.sh
dev
and ./run-local.sh
stop
commands respectively.
Using Environment Variables
While it’s reasonable in a development environment to store throwaway credentials in plain sight, pushing a hard‑coded database password or an API access token to a production repository is almost never a good idea, even though this sort of thing occurs from time to time on an impressive scale. There are numerous ways to avoid such incidents, but one of the most basic alternatives to hard‑coded credentials are environment variables.
You can set up environment variables in many ways, including:
- Supplying them at startup as arguments to the
docker
run
--env
ordocker
run
--env-file
command - Listing them in a
docker-compose.yaml
file, as in Setting Up Development Environments - Last but not least, using the NGINX Unit configuration API’s
applications
object, specifically itsenvironment
option.In the development version of our NGINX Unit config, we make use of the
environment
option for the two applications, as shown in this example:As you see, the JSON in the
environment
section is rather straightforward: you provide a key‑value list of variable names and their settings for the app. The advantage here is that the values are visible to a single app only; they are not shared with other apps in the same environment.
Of course, the choice of a specific method is up to you. However, we recommend picking a single spot to configure your environment variables and sticking to that choice.
Conclusion
As demonstrated throughout this post, NGINX Unit is packed with features and options that help you boil multi‑tiered scenarios involving numerous tools and technologies down to a few shell commands:
- The configuration is dynamic, which enables updating your app or even its specific sections on the fly.
- The configuration is also granular, so you can fine‑tune individual settings without affecting the entire setup.
- Docker images provide ready‑made solutions that take little to no adjustment, so you can quickly encapsulate any app for standalone use or deploy it in conjunction with other services.
All this makes NGINX Unit a potent candidate for app delivery stacks of any complexity and volume. We are working on several important updates to enhance request routing and app isolation, thus bringing even more options to the table, so stick with us!