NGINX Unit

Unit in Docker§

To run your apps in containerized Unit using the images we provide, you need at least to:

  • Mount your application files to a directory in your container.
  • Publish Unit’s listener port to the host machine.

For example:

$ export UNIT=$(docker run -d \
                       --mount type=bind,src="$(pwd)",dst=/www \
                       -p 8080:8000 nginx/unit:latest)

The command mounts current host directory (where your app files are stored) to the container’s /www directory and publishes the container’s port 8000 (that the listener will use) as port 8080 on the host, saving the container ID in the UNIT environment variable.

Next, you need to upload a configuration to Unit via the control socket:

$ docker exec -ti $UNIT curl -X PUT --data-binary @/www/config.json \
                             --unix-socket var/run/control.unit.sock http://localhost/config

This command assumes that your configuration is stored as config.json in the container-mounted directory on the host. If it has a listener on port 8000, your app is now accessible at port 8080 of the host. For details of Unit configuration, see Configuration Management.

Now for a few detailed scenarios.

Running Apps in Containerized Unit§

Suppose we have a web app with a few dependencies, say Flask’s official hello world app:

$ cd /path/to/app/
$ mkdir webapp
$ cat << EOF > webapp/app.py

    > from flask import Flask
    > app = Flask(__name__)
    >
    > @app.route('/')
    > def hello_world():
    >     return 'Hello, World!'
    > EOF

However basic it is, there’s already a dependency, so let’s put it into a file called requirements.txt:

$ mkdir config
$ cat << EOF > config/requirements.txt

    > flask
    > EOF

Next, create a simple Unit configuration for the app:

# cat << EOF > config/config.json

    > {
    >    "listeners":{
    >       "*:8000":{
    >          "pass":"applications/webapp"
    >       }
    >    },
    >    "applications":{
    >       "webapp":{
    >          "type":"python 3",
    >          "path":"/www/",
    >          "module":"app"
    >       }
    >    }
    > }
    > EOF

Finally, let’s create log and state directories to store Unit log and state respectively:

$ mkdir log
$ touch log/unit.log
$ mkdir state

Our file structure so far:

/path/to/app
├── config
│   ├── config.json
│   └── requirements.txt
├── log
│   └── unit.log
├── state
└── webapp
    └── app.py

Everything is ready for a containerized Unit. First, let’s create a Dockerfile to install app prerequisites:

FROM nginx/unit:latest
COPY config/requirements.txt /config/requirements.txt
RUN apt update && apt install -y python3-pip    \
    && pip3 install -r /config/requirements.txt \
    && rm -rf /var/lib/apt/lists/*
$ docker build --tag=unit-webapp .

Next, we start a container and map it to our directory structure:

$ export UNIT=$(docker run -d \
                       --mount type=bind,src="$(pwd)/config/config.json",dst=/config/config.json \
                       --mount type=bind,src="$(pwd)/log/unit.log",dst=/var/log/unit.log \
                       --mount type=bind,src="$(pwd)/state",dst=/var/lib/unit \
                       --mount type=bind,src="$(pwd)/webapp",dst=/www \
                                   -p 8080:8000 unit-webapp)

Note

With this mapping, Unit will store its state and log in your file structure, essentially making it portable.

Now we can configure the app in Unit:

$ docker exec -ti $UNIT curl -X PUT --data-binary @/config/config.json \
                             --unix-socket /var/run/control.unit.sock http://localhost/config

    {
        "success": "Reconfiguration done."
    }

Finally, let’s test the app:

$ curl -X GET localhost:8080

    Hello, World!

To relocate the app in your filesystem, you only need to move the file structure:

$ mv /path/to/app /new/path/to/app

To switch your app to another Unit image, prepare a corresponding Dockerfile first:

FROM nginx/unit:1.9.0-python3.5
COPY config/requirements.txt /config/requirements.txt
RUN apt update && apt install -y python3-pip    \
    && pip3 install -r /config/requirements.txt \
    && rm -rf /var/lib/apt/lists/*
$ docker build --tag=unit-pruned-webapp .

Run a container from the new image; Unit picks up the mapped state automatically:

$ export UNIT=$(docker run -d \
                       --mount type=bind,src="$(pwd)/log/unit.log",dst=/var/log/unit.log \
                       --mount type=bind,src="$(pwd)/state",dst=/var/lib/unit \
                       --mount type=bind,src="$(pwd)/webapp",dst=/www \
                                   -p 8080:8000 unit-pruned-webapp)

Containerizing Apps§

Suppose you have a Unit-ready Express app:

#!/usr/bin/env node

const {
  createServer,
  IncomingMessage,
  ServerResponse,
} = require('unit-http')

require('http').ServerResponse = ServerResponse
require('http').IncomingMessage = IncomingMessage

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello, Unit!'))

createServer(app).listen()

Its Unit configuration, stored as config.json:

{
    "listeners": {
        "*:8080": {
            "pass": "applications/express_app"
        }
    },

    "applications": {
        "express_app": {
            "type": "external",
            "working_directory": "/www/",
            "executable": "app.js"
        }
    }
}

Resulting file structure:

myapp/
├── app.js
└── config.json

Let’s prepare a Dockerfile to install and configure the app in an image, layering it to benefit from caching:

# keep our base image as small as possible
FROM nginx/unit:1.9.0-minimal

# add NGINX Unit and Node.js repos
RUN apt update                                                             \
    && apt install -y apt-transport-https gnupg1                           \
    && curl https://nginx.org/keys/nginx_signing.key | apt-key add -       \
    && echo "deb https://packages.nginx.org/unit/debian/ stretch unit"     \
         > /etc/apt/sources.list.d/unit.list                               \
    && echo "deb-src https://packages.nginx.org/unit/debian/ stretch unit" \
         >> /etc/apt/sources.list.d/unit.list                              \
    && curl https://deb.nodesource.com/setup_12.x | bash -                 \
# install build chain
    && apt update                                                          \
    && apt install -y build-essential nodejs unit-dev                      \
# add global dependencies
    && npm install -g --unsafe-perm unit-http                              \
# final cleanup
    && apt remove -y build-essential unit-dev apt-transport-https gnupg1   \
    && apt clean && apt autoclean && apt autoremove --purge -y             \
    && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list

# same as "working_directory" in config.json
COPY myapp/ /www

# port used by the listener in config.json
EXPOSE 8080

# add app dependencies locally
RUN cd /www && npm link unit-http && npm install express                   \
# launch Unit for initial app config
    && unitd --control unix:/var/run/control.unit.sock                     \
# configure the app in Unit
    && curl -X PUT --data-binary @/www/config.json --unix-socket           \
        /var/run/control.unit.sock http://localhost/config/                \
# app dir cleanup
    && rm /www/config.json
$ docker build --tag=unit-expressapp .

Subsequent start in a container will make Unit pick up the initial config you’ve uploaded during the build:

$ docker run -d -p 8080:8080 unit-expressapp
$ curl -X GET localhost:8080

     Hello, Unit!

This approach is applicable to any Unit-supported apps with external dependencies.

Finally, to reconfigure the app in an existing container, simply supply the config either as a file or explicitly:

$ export UNIT=$(docker run -d --mount type=bind,src="$(pwd)",dst=/cfg unit-expressapp)
$ docker exec -ti $UNIT curl -X PUT --data-binary @/cfg/config.json \
                             --unix-socket /var/run/control.unit.sock http://localhost/config
$ docker exec -ti $UNIT curl -X PUT -d '"/www/newapp/"' --unix-socket \
                             /var/run/control.unit.sock http://localhost/config/applications/express_app/working_directory