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.

Note

For app containerization examples, refer to our sample Go, Java, Node.js, Perl, PHP, Python, and Ruby Dockerfiles; also, see a more elaborate discussion below.

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__)
    application = app

    @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:

$ cat << EOF > requirements.txt

    flask
    EOF

Next, create a simple Unit configuration for the app:

$ mkdir config
$ 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
├── log
│   └── unit.log
├── requirements.txt
├── 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 requirements.txt /config/requirements.txt
RUN apt update && apt install -y python3-pip                               \
    && pip3 install -r /config/requirements.txt                            \
    && apt remove -y python3-pip                                           \
    && apt autoremove --purge -y                                           \
    && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list
$ 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/",dst=/docker-entrypoint.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-webapp)

Note

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

We’ve mapped the source config/ to /docker-entrypoint.d/ in the container; the official image uploads any .json files found there into Unit’s config section if the state is empty. Now we can 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.12.0-python3.5
COPY 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:

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

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

# 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                              \
# add app dependencies locally
    && cd /www && npm link unit-http && npm install express                \
# final cleanup
    && apt remove -y build-essential unit-dev apt-transport-https gnupg1   \
    && apt autoremove --purge -y                                           \
    && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list

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

When you start a container based on this image, mount the config.json file to initialize Unit’s state:

$ docker build --tag=unit-expressapp .
$ export UNIT=$(docker run -d --mount \
      type=bind,src="$(pwd)/myapp/config.json",dst=/docker-entrypoint.d/config.json \
      -p 8080:8080 unit-expressapp)
$ curl -X GET localhost:8080

     Hello, Unit!

Note

This mechanism allows to initialize Unit at container startup only if its state is empty; otherwise, the contents of /docker-entrypoint.d/ is ignored. Continuing the previous sample:

$ docker commit $UNIT unit-expressapp  # store non-empty Unit state in the image
# cat << EOF > myapp/new-config.json   # let's attempt re-initialization
  ...
  EOF
$ export UNIT=$(docker run -d --mount \
      type=bind,src="$(pwd)/myapp/new-config.json",dst=/docker-entrypoint.d/new-config.json \
      -p 8080:8080 unit-expressapp)

Here, Unit will not pick up the new-config.json from the /docker-entrypoint.d/ directory when we run a container from the updated image because Unit’s state was initialized and saved earlier.

To configure the app after startup, supply a file or an explicit snippet via the config API:

$ cat << EOF > myapp/new-config.json
  ...
  EOF
$ export UNIT=$(docker run -d --mount \
      type=bind,src="$(pwd)/myapp/new-config.json",dst=/cfg/new-config.json unit-expressapp)
$ docker exec -ti $UNIT curl -X PUT --data-binary @/cfg/new-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

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