NGINX Unit
v. 1.22.0

Unit in Docker§

To run your apps in a 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:1.22.0-python3.9               \
  )

The command mounts the host’s current 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’s ID in the UNIT environment variable.

Next, 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 your configuration is stored as config.json in the container-mounted directory on the host; if the file defines a listener on port 8000, your app is now accessible on port 8080 of the host. For details of the 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.

Apps in a 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/wsgi.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 list it in 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": "wsgi",
             "callable": "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
    └── wsgi.py

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

FROM nginx/unit:1.22.0-python3.9
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 stores its state and log in your file structure. By default, our Docker images forward their log output to the Docker log collector.

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 a different Unit image, prepare a corresponding Dockerfile first:

FROM nginx/unit:1.22.0-python3.9
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                                    \
  )

Containerized 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"
        }
    },

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

The resulting file structure:

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

Note

Don’t forget to chmod +x the app.js file so Unit can run it.

Let’s prepare a Dockerfile to install and configure the app in an image:

# keep our base image as specific as possible
FROM nginx/unit:1.22.0-node15

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

# 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 a 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 does 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/working_directory

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