NGINX Unit

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 unit:1.34.0-python3.11                 \
  )

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.

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 unit:1.34.0-python3.11
COPY requirements.txt /config/requirements.txt
RUN python3 -m pip install -r /config/requirements.txt
$ 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 file system, 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 unit:1.34.0-minimal
COPY requirements.txt /config/requirements.txt
# This time, we took a minimal Unit image to install a vanilla Python 3.9
# module, run PIP, and perform cleanup just like we did earlier.

# First, we install the required tooling and add Unit's repo.
RUN apt update && apt install -y curl apt-transport-https gnupg2 lsb-release  \
    &&  curl -o /usr/share/keyrings/nginx-keyring.gpg                         \
           https://unit.nginx.org/keys/nginx-keyring.gpg                      \
    && echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg]            \
           https://packages.nginx.org/unit/debian/ `lsb_release -cs` unit"    \
           > /etc/apt/sources.list.d/unit.list

# Next, we install the module, download app requirements, and perform cleanup.
RUN apt update && apt install -y unit-python3.9 python3-pip                   \
    && python3 -m pip install -r /config/requirements.txt                     \
    && apt remove -y curl apt-transport-https gnupg2 lsb-release python3-pip  \
    && apt autoremove --purge -y                                              \
    && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list
$ 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 as app.js:

#!/usr/bin/env node

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

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

http.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": "/usr/bin/env",
            "arguments": [
                "node",
                "--loader",
                "unit-http/loader.mjs",
                "--require",
                "unit-http/loader",
                "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 unit:1.34.0-node15

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

# Install and link Express in the app directory.
RUN cd /www && npm install express && npm link unit-http

# 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 control 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.

Multilanguage Images§

Earlier, Unit had a -full Docker image with modules for all supported languages, but it was discontinued with version 1.22.0. If you still need a multilanguage image, use the following Dockerfile template that starts with the minimal Unit image based on Debian 11 and installs official language module packages:

FROM unit:1.34.0-minimal
# We take a minimal Unit image and install language-specific modules.

# First, we install the required tooling and add Unit's repo.
RUN apt update && apt install -y curl apt-transport-https gnupg2 lsb-release  \
    &&  curl -o /usr/share/keyrings/nginx-keyring.gpg                         \
           https://unit.nginx.org/keys/nginx-keyring.gpg                      \
    && echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg]            \
           https://packages.nginx.org/unit/debian/ `lsb_release -cs` unit"    \
           > /etc/apt/sources.list.d/unit.list

# Next, we install the necessary language module packages and perform cleanup.
RUN apt update && apt install -y                                              \
        unit-jsc11 unit-perl unit-php unit-python2.7 unit-python3.9 unit-ruby \
    && apt remove -y curl apt-transport-https gnupg2 lsb-release              \
    && apt autoremove --purge -y                                              \
    && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/*.list

Instead of packages, you can build custom modules; use these Dockerfile.* templates as reference.

Startup Customization§

Finally, you can customize the way Unit starts in a container by adding a new Dockerfile layer:

FROM unit:1.34.0-minimal

CMD ["unitd-debug","--no-daemon","--control","unix:/var/run/control.unit.sock"]

The CMD instruction above replaces the default unitd executable with its debug version. Use Unit’s command-line options to alter its startup behavior, for example:

FROM unit:1.34.0-minimal

CMD ["unitd","--no-daemon","--control","0.0.0.0:8080"]

This replaces Unit’s default UNIX domain control socket with an IP socket address.