NGINX Unit
v. 1.24.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.24.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.24.0-python3.9
COPY requirements.txt /config/requirements.txt
# PIP isn't installed by default, so we install it first.
# Next, we install the requirements, remove PIP, and perform image cleanup.
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.24.0-minimal
COPY requirements.txt /config/requirements.txt
# This time, we took a minimal Unit image to install a vanilla Python 3.7
# module, run PIP and perform cleanup just like we did earlier.

# First, we install the tooling required to add Unit's repo and import its key.
RUN apt update && apt install -y curl apt-transport-https gnupg1 lsb-release  \
    && curl -sL https://nginx.org/keys/nginx_signing.key | apt-key add -

# Next, we add Unit's repo, install the module, and perform creanup.
RUN echo "deb https://packages.nginx.org/unit/debian/ `lsb_release -cs` unit" \
         > /etc/apt/sources.list.d/unit.list                                  \
    && apt update && apt install -y unit-python3.7 python3-pip                \
    && pip3 install -r /config/requirements.txt                               \
    && apt remove -y curl apt-transport-https gnupg1 lsb-release              \
    && 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 stored in the myapp/ directory:

#!/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 in the same directory:

{
    "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.24.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.