NGINX Unit
v. 1.21.0

Configuration§

Quick Start§

To run an application in Unit, first set up an application object. Let’s store it in a file to PUT it into the config/applications section of Unit’s control API, available via the control socket at http://localhost/:

$ cat << EOF > config.json

    {
        "type": "php",
        "root": "/www/blogs/scripts"
    }
    EOF

# curl -X PUT --data-binary @config.json --unix-socket \
       /path/to/control.unit.sock http://localhost/config/applications/blogs

    {
        "success": "Reconfiguration done."
    }

Unit starts the application process. Next, reference the application object from a listener object, comprising an IP (or a wildcard to match any IPs) and a port number, in the config/listeners section of the API:

$ cat << EOF > config.json

    {
        "pass": "applications/blogs"
    }
    EOF

# curl -X PUT --data-binary @config.json --unix-socket \
       /path/to/control.unit.sock http://localhost/config/listeners/127.0.0.1:8300

    {
        "success": "Reconfiguration done."
    }

Unit accepts requests at the specified IP and port, passing them to the application process. Your app works!

Finally, check the resulting configuration:

# curl --unix-socket /path/to/control.unit.sock http://localhost/config/

    {
        "listeners": {
            "127.0.0.1:8300": {
                "pass": "applications/blogs"
            }
        },

        "applications": {
            "blogs": {
                "type": "php",
                "root": "/www/blogs/scripts/"
            }
        }
    }

You can upload the entire configuration at once or update it in portions. For details of configuration techniques, see below. For a full configuration sample, see here.

Configuration Management§

Unit’s configuration is JSON-based, accessed via the control socket, and entirely manageable over HTTP.

Note

Here, we use curl to query Unit’s control API, prefixing URIs with http://localhost as expected by this utility. You can use any tool capable of making HTTP requests; also, the hostname is irrelevant for Unit.

To address parts of the configuration, query the control socket over HTTP; URI path segments of your requests to the API must be names of its JSON object members or indexes of its array elements.

You can manipulate the API with the following HTTP methods:

MethodAction
GETReturns the entity at the request URI as a JSON value in the HTTP response body.
POSTUpdates the array at the request URI, appending the JSON value from the HTTP request body.
PUTReplaces the entity at the request URI and returns status message in the HTTP response body.
DELETEDeletes the entity at the request URI and returns status message in the HTTP response body.

Before a change, Unit evaluates the difference it causes in the entire configuration; if there’s none, nothing is done. For example, you can’t restart an updated app by uploading the same configuration it already has.

Note

While we’re working on handy app reload control, there’s a workaround to forcefully restart an app in Unit by updating an environment variable. First, check whether the app has an environment object:

# curl --unix-socket /path/to/control.unit.sock \
       http://localhost/config/applications/app/environment

      {
          "error": "Value doesn't exist."
      }

Here, it doesn’t, so you can safely add a new variable with a shell-interpolated value:

# curl -X PUT -d '{"APPGEN":"'$(date +"%s")'"}' --unix-socket \
       /path/to/control.unit.sock http://localhost/config/applications/app/environment

Otherwise, take care and target the individual variable to avoid overwriting the entire environment:

# curl -X PUT -d '"'$(date +"%s")'"' --unix-socket \
       /path/to/control.unit.sock http://localhost/config/applications/app/environment/APPGEN

To make Unit reload the app, repeat the PUT command, updating the APPGEN variable.

Unit performs actual reconfiguration steps as gracefully as possible: running tasks expire naturally, connections are properly closed, processes end smoothly.

Any type of update can be done with different URIs, provided you supply the right JSON:

# curl -X PUT -d '{ "pass": "applications/blogs" }' --unix-socket \
       /path/to/control.unit.sock http://localhost/config/listeners/127.0.0.1:8300

# curl -X PUT -d '"applications/blogs"' --unix-socket /path/to/control.unit.sock \
       http://localhost/config/listeners/127.0.0.1:8300/pass

However, mind that the first command replaces the entire listener, dropping any other options you could have configured, whereas the second one replaces only the pass value and leaves other options intact.

Examples

To minimize typos and effort, avoid embedding JSON payload in your commands; instead, consider storing your configuration snippets for review and reuse. Suppose you save your application object as wiki.json:

{
    "type": "python",
    "module": "wsgi",
    "user": "www-wiki",
    "group": "www-wiki",
    "path": "/www/wiki/"
}

Use it to set up an application called wiki-prod:

# curl -X PUT --data-binary @/path/to/wiki.json \
       --unix-socket /path/to/control.unit.sock http://localhost/config/applications/wiki-prod

Use it again to set up a development version of the same app called wiki-dev:

# curl -X PUT --data-binary @/path/to/wiki.json \
       --unix-socket /path/to/control.unit.sock http://localhost/config/applications/wiki-dev

Toggle the wiki-dev app to another source code directory:

# curl -X PUT -d '"/www/wiki-dev/"' \
       --unix-socket /path/to/control.unit.sock http://localhost/config/applications/wiki-dev/path

Next, boost the process count for the production app to warm it up a bit:

# curl -X PUT -d '5' \
       --unix-socket /path/to/control.unit.sock http://localhost/config/applications/wiki-prod/processes

Add a listener for the wiki-prod app to accept requests at all host IPs:

# curl -X PUT -d '{ "pass": "applications/wiki-prod" }' \
       --unix-socket /path/to/control.unit.sock 'http://localhost/config/listeners/*:8400'

Plug the wiki-dev app into the listener to test it:

# curl -X PUT -d '"applications/wiki-dev"' --unix-socket /path/to/control.unit.sock \
       'http://localhost/config/listeners/*:8400/pass'

Then rewire the listener, adding a URI-based route to the development version of the app:

$ cat << EOF > config.json

    [
        {
            "match": {
                "uri": "/dev/*"
            },

            "action": {
                "pass": "applications/wiki-dev"
            }
        }
    ]
    EOF

# curl -X PUT --data-binary @config.json --unix-socket \
       /path/to/control.unit.sock http://localhost/config/routes

# curl -X PUT -d '"routes"' --unix-socket \
       /path/to/control.unit.sock 'http://localhost/config/listeners/*:8400/pass'

Next, let’s change the wiki-dev’s URI prefix in the routes array using its index (0):

# curl -X PUT -d '"/development/*"' --unix-socket=/path/to/control.unit.sock \
       http://localhost/config/routes/0/match/uri

Let’s add a route to the prod app: POST always adds to the array end, so there’s no need for an index:

# curl -X POST -d '{"match": {"uri": "/production/*"}, \
       "action": {"pass": "applications/wiki-prod"}}'  \
       --unix-socket=/path/to/control.unit.sock        \
       http://localhost/config/routes/

Otherwise, use PUT with the array’s last index (0 in our sample) plus one to add the new item at the end:

# curl -X PUT -d '{"match": {"uri": "/production/*"}, \
       "action": {"pass": "applications/wiki-prod"}}' \
       --unix-socket=/path/to/control.unit.sock       \
       http://localhost/config/routes/1/

To get the complete config section:

# curl --unix-socket /path/to/control.unit.sock http://localhost/config/

    {
        "listeners": {
            "*:8400": {
                "pass": "routes"
            }
        },

        "applications": {
            "wiki-dev": {
                "type": "python",
                "module": "wsgi",
                "user": "www-wiki",
                "group": "www-wiki",
                "path": "/www/wiki-dev/"
            },

            "wiki-prod": {
                "type": "python",
                "processes": 5,
                "module": "wsgi",
                "user": "www-wiki",
                "group": "www-wiki",
                "path": "/www/wiki/"
            }
        },

        "routes": [
            {
                "match": {
                    "uri": "/development/*"
                },

                "action": {
                    "pass": "applications/wiki-dev"
                }
            },
            {
                "action": {
                    "pass": "applications/wiki-prod"
                }
            }
        ]
    }

To obtain the wiki-dev application object:

# curl --unix-socket /path/to/control.unit.sock \
       http://localhost/config/applications/wiki-dev

    {
        "type": "python",
        "module": "wsgi",
        "user": "www-wiki",
        "group": "www-wiki",
        "path": "/www/wiki-dev/"
    }

You can save JSON returned by such requests as .json files for update or review:

# curl --unix-socket /path/to/control.unit.sock \
       http://localhost/config/ > config.json

To drop the listener on *:8400:

# curl -X DELETE --unix-socket /path/to/control.unit.sock \
       'http://localhost/config/listeners/*:8400'

Mind that you can’t delete objects that other objects rely on, such as a route still referenced by a listener:

# curl -X DELETE --unix-socket /var/run/unit/control.sock \
        http://localhost/config/routes

     {
         "error": "Invalid configuration.",
         "detail": "Request \"pass\" points to invalid location \"routes\"."
     }

Listeners§

To start accepting requests, add a listener object in the config/listeners API section. The object’s name uniquely combines a host IP address and a port that Unit binds to; a wildcard matches any host IPs.

Note

On Linux-based systems, wildcard listeners can’t overlap with other listeners on the same port due to kernel-imposed limitations; for example, *:8080 conflicts with 127.0.0.1:8080.

Unit dispatches the requests it receives to destinations referenced by listeners. You can plug several listeners into one destination or use a single listener and hot-swap it between multiple destinations.

Available listener options:

OptionDescription
pass

Destination to which the listener passes incoming requests. Possible alternatives:

Note

The value is variable-interpolated; if it matches no configuration entities after interpolation, a 404 “Not Found” response is returned.

tlsSSL/TLS configuration object. Its only option, certificate, enables secure communication via the listener; it must name a certificate chain that you have configured earlier.

Here, a local listener accepts requests at port 8300 and passes them to the blogs app target identified by the uri variable. The wildcard listener on port 8400 is protected by the blogs-cert certificate bundle and relays requests at any host IPs to the main route:

{
    "127.0.0.1:8300": {
        "pass": "applications/blogs$uri"
    },

    "*:8400": {
        "pass": "routes/main",
        "tls": {
            "certificate": "blogs-cert"
        }
    }
}

Also, the pass values can be percent encoded. For example, you can escape slashes in entity names:

{
    "listeners": {
         "*:80": {
             "pass": "routes/slashes%2Fin%2Froute%2Fname"
         }
    },

    "routes": {
         "slashes/in/route/name": []
    }
}

Routes§

The config/routes configuration entity defines internal request routing, receiving requests via listeners and filtering them through sets of conditions to be processed by apps, proxied to external servers or load-balanced between them, served with static content, answered with arbitrary status codes, or redirected.

In its simplest form, routes can be a single route array:

{
     "listeners": {
         "*:8300": {
             "pass": "routes"
         }
     },

     "routes": [ "simply referred to as routes" ]
}

Another form is an object with one or more named route arrays as members:

{
     "listeners": {
         "*:8300": {
             "pass": "routes/main"
         }
     },

     "routes": {
         "main": [ "named route, qualified name: routes/main" ],
         "route66": [ "named route, qualified name: routes/route66" ]
     }
}

Route Steps§

A route array contains step objects as elements; a request passed to a route traverses them sequentially:

  • If the request meets all match conditions in a step, the step’s action is performed.
  • If the request doesn’t match a step’s condition, Unit proceeds to the next step of the route.
  • If the request doesn’t match any steps of the route, a 404 “Not Found” response is returned.

Step objects accept the following options:

OptionDescription
action (required)Object that defines how matching requests are handled.
matchObject that defines the step’s conditions to be matched.

Warning

If a step omits the match option, its action is performed automatically. Thus, use no more than one such step per route, always placing it last to avoid potential routing issues.

Examples

A basic one:

{
    "routes": [
        {
            "match": {
                "host": "example.com",
                "scheme": "https",
                "uri": "/php/*"
            },

            "action": {
                "pass": "applications/php_version"
            }
        },
        {
            "action": {
                "share": "/www/static_version/"
            }
        }
    ]
}

A more elaborate example with chained routes and proxying:

{
    "routes": {
        "main": [
            {
                "match": {
                    "scheme": "http"
                },

                "action": {
                    "pass": "routes/http_site"
                }
            },
            {
                "match": {
                    "host": "blog.example.com"
                },

                "action": {
                    "pass": "applications/blog"
                }
            },
            {
                 "match": {
                     "uri": [
                         "*.css",
                         "*.jpg",
                         "*.js"
                     ]
                 },
                "action": {
                    "share": "/www/static/"
                }
            },
            {
                "action": {
                    "return": 404
                }
            }
        ],

        "http_site": [
            {
                "match": {
                    "uri": "/v2_site/*"
                },

                "action": {
                    "pass": "applications/v2_site"
                }
            },
            {
                "action": {
                    "proxy": "http://127.0.0.1:9000"
                }
            }
        ]
    }
}

Condition Matching§

To route incoming requests, Unit applies pattern-based conditions to individual request properties:

OptionMatched AgainstCase‑SensitiveMatch Type
argumentsParameter arguments supplied in the request target query. Names and values can be percent encoded.YesCompound
cookiesCookies supplied with the request.YesCompound
destinationTarget IP address and optional port of the request.NoSimple
headersHeader fields supplied with the request.NoCompound
hostHost header field without the port number and the trailing period (if any).NoSimple
methodMethod from the request line.NoSimple
schemeURI scheme. Currently, http and https are supported.Nohttp/https
sourceSource IP address and optional port of the request.NoSimple
uriRequest target path without the query part, normalized by resolving relative path references (“.” and “..”) and collapsing adjacent slashes. Can be percent encoded.YesSimple

Note

Both arguments and uri support percent encoding. Thus, you can escape characters which have special meaning in routing (! is %21, * is %2A, % is %25), or even target individual bytes. For example, to select an entire class of diacritic characters such as Ö or Å by their starting byte 0xC3 in UTF-8:

{
    "arguments": {
        "word": "*%C3*"
    }
}

This requires mentioning that actual arguments and URIs passed with requests are percent decoded: Unit interpolates all percent-encoded entities in these properties. Thus, the following configuration:

{
    "routes": [
        {
            "match": {
                "uri": "/static files/*"
            },

            "action": {
                "share": "/www/data/"
            }
        }
    ]
}

Matches this percent-encoded request:

$ curl http://127.0.0.1/static%20files/test.txt -v

      > GET /static%20files/test.txt HTTP/1.1
      ...
      < HTTP/1.1 200 OK
      ...

Simple Matching§

A simple property in a match object is matched against a string pattern or an array of patterns:

{
    "match": {
        "simple_property1": "pattern",
        "simple_property2": ["pattern", "pattern", "..." ]
    },

    "action": {
        "pass": "..."
    }
}

To be a match against the condition, the property must meet two requirements:

  • If there are patterns without negation, at least one of them matches the property value.
  • No negation-based patterns match the property value.

Patterns must be exact matches. Regexes (~), negations (!), wildcards (*), and ranges (-) can be used:

  • A negation can only start a pattern; it rejects all matches to its remainder (!<negated_pattern>).
  • In host, method, and uri, a wildcard matches any number of characters, and an arbitrary number of wildcards can be used in a single pattern: How*s*that*to*you?. However, ranges are not supported.
  • In source and destination, wildcards can only be used to match any IPs (*:<port>). Also, ranges can be used to specify IPs (in respective notation) and ports (<start_port>-<end_port>).
  • A regex pattern starts with a tilde, optionally preceded by a negation: !~^\\d+\\.\\d+\\.\\d+\\.\\d+$ (note the escaping; this is a JSON requirement). By default, regexes use the PCRE syntax; see the compilation options for details. However, source, destination, and scheme can’t use regexes.

Note

This type of matching can be explained with set operations. Suppose set U comprises all possible values of a property; set P comprises strings that match any patterns without negation; set N comprises strings that match any negation-based patterns. In this scheme, the matching set will be:

UP \ N if P ≠ ∅
U \ N if P = ∅
Examples
{
    "uri": "~^/data/www/.*\\.php(/.*)?$"
}

A regular expression that matches any .php files within the /data/www/ directory and its subdirectiories. Note the backslashes; escaping is a JSON-specific requirement.

{
    "host": "*.example.com"
}

Only subdomains of example.com will match.

{
    "uri": "/admin/*/*.php"
}

Only requests for .php files located in /admin/’s subdirectories will match.

{
    "host": ["eu-*.example.com", "!eu-5.example.com"]
}

Here, any eu- subdomains of example.com will match except eu-5.example.com.

{
    "method": ["!HEAD", "!GET"]
}

Any methods will match except HEAD and GET.

You can also combine special characters in a pattern:

{
    "uri": "!*/api/*"
}

Here, any URIs will match except the ones containing /api/.

Individual addresses and address ranges can be specified in dot-decimal or CIDR notation for IPv4:

{
    "match": {
        "source": [
             "10.0.0.0-10.0.0.10",
             "10.0.0.100-11.0.0.100:1000",
             "127.0.0.100-127.0.0.255:8080-8090"
        ],
        "destination": [
            "10.0.0.0/8",
            "10.0.0.0/7:1000",
            "10.0.0.0/32:8080-8090"
        ]
    }
}

Or IPv6:

{
    "match": {
        "source": [
             "2001::-200f:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
             "[fe08::-feff::]:8000",
             "[fff0::-fff0::10]:8080-8090"
        ],
        "destination": [
             "2001::/16",
             "[0ff::/64]:8000",
             "[fff0:abcd:ffff:ffff:ffff::/128]:8080-8090"
         ]
    }
}

Compound Matching§

This type of matching is used for arguments, cookies, and headers properties.

A compound property is matched against an object with names and patterns or an array of such objects:

{
    "match": {
        "compound_property1": {
            "name1": "pattern",
            "name2": ["pattern", "..."]
        },

        "compound_property2": [
            {
                "name1": "pattern",
                "name2": ["pattern", "pattern", "..."]
            },
            {
                "name1": "pattern",
                "name3": ["pattern", "pattern", "..."]
            }
        ]
    },

    "action": {
        "pass": "..."
    }
}

To match a single condition object, the request must contain all items explicitly named in the object; their values are matched against patterns in the same manner as property values during simple matching.

To match an object array, it’s sufficient to match any single one of its objects.

Examples
{
    "arguments": {
        "mode": "strict",
        "access": "!full"
    }
}

This requires mode=strict and any access argument other than access=full in the URI.

{
    "headers": [
        {
            "Accept-Encoding": "*gzip*",
            "User-Agent": "Mozilla/5.0*"
        },

        {
            "User-Agent": "curl*"
        }
    ]
}

This matches all requests that either use gzip and identify as Mozilla/5.0 or list curl as the user agent.

Request Handling§

If a request matches all conditions of a route step, or the step itself omits the match object, Unit handles the request using the respective action. Possible combinations of action options are:

pass

Route’s destination upon a match, identical to pass in a listener.

Read more: Listeners.

share, fallback

The share is a static pathname from where files are served upon a match. The optional fallback action (identical to match/action) is performed if the requested file isn’t found or can’t be accessed. Thus, share-based fallback actions can be nested.

Read more: Static Files.

proxy

Socket address of an HTTP server where the request is proxied upon a match.

Read more: Proxying.

return, location

The return value defines the HTTP response status code to be returned upon a match. The location is required if the return value implies redirection (3xx).

Read more: Instant Responses and Redirects.

An example:

{
    "routes": [
        {
            "match": {
                "uri": "/pass/*"
            },

            "action": {
                "pass": "applications/app"
            }
        },
        {
            "match": {
                "uri": "~\\.jpe?g$"
            },

            "action": {
                "share": "/var/www/static/",
                "fallback": {
                    "share": "/var/www/static/assets",
                    "fallback": {
                         "pass": "upstreams/cdn"
                    }
                }
            }
        },
        {
            "match": {
                "uri": "/proxy/*"
            },

            "action": {
                "proxy": "http://192.168.0.100:80"
            }
        },
        {
            "match": {
                "uri": "/return/*"
            },

            "action": {
                "return": 301,
                "location": "https://www.example.com"
            }
        }
    ]
}

Instant Responses and Redirects§

You can configure route actions to instantly respond to certain conditions with arbitrary HTTP status codes:

{
    "match": {
        "uri": "/admin_console/*"
    },

    "action": {
        "return": 403
    }
}

The return option accepts any integer values within the 000-999 range. It is recommended to use the codes according to their semantics; if you use custom codes, make sure user agents can understand them.

If you specify a redirect code (3xx), supply the destination using the location option alongside return:

{
    "action": {
        "return": 301,
        "location": "https://www.example.com"
    }
}

Variables§

While configuring Unit, you can use built-in variables that are replaced by dynamic values in runtime. This enables flexible request processing, making the configuration more compact and straightforward.

Note

Currently, the only place where variables are recognized is the pass option in listeners and actions. This means you can use them to guide requests between sets of routes, applications, targets, or upstreams.

Available variables:

VariableDescription
hostHost header field in lowercase, without the port number and the trailing period (if any).
methodMethod from the request line.
uriRequest target path without the query part, normalized by resolving relative path references (“.” and “..”) and collapsing adjacent slashes. The value is percent decoded: Unit interpolates all percent-encoded entities in the request target path.

To reference a variable, prefix its name with the dollar sign character ($), optionally enclosing the name in curly brackets ({}) to separate it from adjacent text or enhance visibility. Variable names can contain letters and underscores (_), so use the brackets if the variable is immediately followed by these characters:

{
    "listeners": {
        "*:80": {
            "pass": "routes/${method}_route"
        }
    },

    "routes": {
        "GET_route": [
            {
                "action": {
                    "return": 201
                }
            }
        ],

        "PUT_route": [
            {
                "action": {
                    "return": 202
                }
            }
        ],

        "POST_route": [
            {
                "action": {
                    "return": 203
                }
            }
        ]
    }
}

At runtime, variables are replaced by dynamically computed values (at your risk!). For example, the listener above targets an entire set of routes, picking individual ones by HTTP verbs that the incoming requests use:

$ curl -i -X GET http://localhost

    HTTP/1.1 201 Created

$ curl -i -X PUT http://localhost

    HTTP/1.1 202 Accepted

$ curl -i -X POST http://localhost

    HTTP/1.1 203 Non-Authoritative Information

$ curl -i --head http://localhost  # Bumpy ride ahead, no route defined

    HTTP/1.1 404 Not Found

Another obvious usage is employing the URI to choose between applications:

{
    "listeners": {
        "*:80": {
            "pass": "applications$uri"
        }
    },

    "applications": {
        "blog": {
            "root": "/path/to/blog_app/",
            "script": "index.php"
        },

        "sandbox": {
            "type": "php",
            "root": "/path/to/sandbox_app/",
            "script": "index.php"
        }
    }
}

This way, we can route requests to applications by request target URIs. A different approach can route requests between applications by the Host header field received from the client:

{
    "listeners": {
        "*:80": {
            "pass": "applications/$host"
        }
    },

    "applications": {
        "localhost": {
            "root": "/path/to/admin_section/",
            "script": "index.php"
        },

        "www.example.com": {
            "type": "php",
            "root": "/path/to/public_app/",
            "script": "index.php"
        }
    }
}

You can combine variables as you see fit, repeating them or placing them in arbitrary order. This configuration picks application targets by their names and request methods:

{
    "listeners": {
        "*:80": {
            "pass": "applications/app${uri}_${method}"
        }
    }
}

Static Files§

Unit is capable of acting as a standalone web server, serving requests for static assets from directories you configure; to use the feature, supply the directory path in the share option of a route step:

{
    "listeners": {
        "127.0.0.1:8300": {
            "pass": "routes"
        }
     },

    "routes": [
        {
            "action": {
                "share": "/www/data/static/"
             }
        }
    ]
}

Suppose the /www/data/static/ directory has the following structure:

/www/data/static/
├── stylesheet.css
├── html
│   └──index.html
└── js files
    └──page.js

In the above configuration, you can request specific files by these URIs:

$ curl 127.0.0.1:8300/html/index.html
$ curl 127.0.0.1:8300/stylesheet.css
$ curl '127.0.0.1:8300/js files/page.js'

Note

Unit supports encoded symbols in URIs as the last query above suggests.

If your query specifies only the directory name, Unit will attempt to serve index.html from this directory:

$ curl -vL 127.0.0.1:8300/html/

 ...
 < HTTP/1.1 200 OK
 < Last-Modified: Fri, 20 Sep 2019 04:14:43 GMT
 < ETag: "5d66459d-d"
 < Content-Type: text/html
 < Server: Unit/1.21.0
 ...

Note

Unit’s ETag response header fields use the following format: %MTIME_HEX%-%FILESIZE_HEX%.

Unit maintains a number of built-in MIME types like text/plain or text/html; also, you can add extra types and override built-ins in the /config/settings/http/static/mime_types section.

Finally, within an action, you can supply a fallback option beside a share. It specifies the action to be taken if the requested file isn’t found at the share path:

{
    "share": "/data/www/",
    "fallback": {
        "pass": "applications/php"
    }
}

In the example above, an attempt to serve the requested file from the /data/www/ directory is made first. Only if there’s no such file, the request is passed to the php application.

If a fallback itself is a share, it can also contain a nested fallback:

{
    "share": "/data/www/",
    "fallback": {
        "share": "/data/cache/",
        "fallback": {
            "proxy": "http://127.0.0.1:9000"
        }
    }
}

First, this configuration tries to serve a file from the /data/www/ directory; next, it queries the /data/cache/ path. If both attempts fail, the request is proxied to an external server.

Examples

One common use case that this feature enables is the separation of requests for static and dynamic content into independent routes. The following example relays all requests that target .php files to an application and uses a catch-all static share with a fallback:

{
    "routes": [
        {
            "match": {
                "uri": "*.php"
            },
            "action": {
                "pass": "applications/php-app"
            }
        },
        {
            "action": {
                "share": "/www/php-app/assets/files/",
                "fallback": {
                    "proxy": "http://127.0.0.1:9000"
                }
            }
        }

    ],

    "applications": {
        "php-app": {
            "type": "php",
            "root": "/www/php-app/scripts/"
        }
    }
}

You can reverse this scheme for apps that avoid filenames in dynamic URIs, listing all types of static content to be served from a share in a match condition and adding an unconditional application path:

{
    "routes": [
        {
            "match": {
                "uri": [
                    "*.css",
                    "*.ico",
                    "*.jpg",
                    "*.js",
                    "*.png",
                    "*.xml"
                ]
            },
            "action": {
                "share": "/www/php-app/assets/files/",
                "fallback": {
                    "proxy": "http://127.0.0.1:9000"
                }
            }
        },
        {
            "action": {
                "pass": "applications/php-app"
            }
        }

    ],

    "applications": {
        "php-app": {
            "type": "php",
            "root": "/www/php-app/scripts/"
        }
    }
}

Proxying§

Unit routes support HTTP proxying to socket addresses using the proxy option of a step’s action:

{
    "routes": [
        {
            "match": {
                "uri": "/ipv4/*"
            },

            "action": {
                "proxy": "http://127.0.0.1:8080"
            }
        },
        {
            "match": {
                "uri": "/ipv6/*"
            },

            "action": {
                "proxy": "http://[::1]:8090"
            }
        },
        {
            "match": {
                "uri": "/unix/*"
            },

            "action": {
                "proxy": "http://unix:/path/to/unix.sock"
            }
        }
    ]
}

As the example above suggests, you can use Unix, IPv4, and IPv6 socket addresses for proxy destinations.

Note

The HTTPS scheme is not supported yet.

Load Balancing§

Besides proxying requests to individual servers, Unit can also relay incoming requests to upstreams. An upstream is a group of servers that comprise a single logical entity and may be used as a pass destination for incoming requests in a listener or a route.

Upstreams are defined in the eponymous config/upstreams section of the API:

{
    "listeners": {
        "*:80": {
            "pass": "upstreams/rr-lb"
        }
    },

    "upstreams": {
        "rr-lb": {
            "servers": {
                "192.168.0.100:8080": { },
                "192.168.0.101:8080": {
                    "weight": 0.5
                }
            }
        }
    }
}

An upstream must define a servers object that lists socket addresses as server object names. Unit dispatches requests between the upstream’s servers in a round-robin fashion, acting as a load balancer. Each server object can set a numeric weight to adjust the share of requests it receives via the upstream. In the above example, 192.168.0.100:8080 receives twice as many requests as 192.168.0.101:8080.

Weights can be specified as integers or fractions in decimal or scientific notation:

{
    "servers": {
        "192.168.0.100:8080": {
            "weight": 1e1
        },
        "192.168.0.101:8080": {
            "weight": 10.0
        },
        "192.168.0.102:8080": {
            "weight": 10
        }
    }
}

The maximum weight is 1000000, the minimum is 0 (such servers receive no requests), the default is 1.

Applications§

Each app that Unit runs is defined as an object in the config/applications section of the control API; it lists the app’s language and settings, its runtime limits, process model, and various language-specific options.

Note

Our official language support packages include end-to-end examples of application configuration, available for your reference at /usr/share/doc/<module name>/examples/ after package installation.

Here, Unit runs 20 processes of a PHP app called blogs, stored in the /www/blogs/scripts/ directory:

{
    "blogs": {
        "type": "php",
        "processes": 20,
        "root": "/www/blogs/scripts/"
    }
}

App objects have a number of options shared between all application languages:

OptionDescription
type (required)

Application type: external (Go and Node.js), java, perl, php, python, or ruby.

Except with external, you can detail the runtime version: "type": "python 3", "type": "python 3.4", or even "type": "python 3.4.9rc1". Unit searches its modules and uses the latest matching one, reporting an error if none match.

For example, if you have only one PHP module, 7.1.9, it matches "php", "php 7", "php 7.1", and "php 7.1.9". If you have modules for versions 7.0.2 and 7.0.23, set "type": "php 7.0.2" to specify the former; otherwise, PHP 7.0.23 will be used.

limitsObject that accepts two integer options, timeout and requests. Their values govern the life cycle of an application process. For details, see here.
processes

Integer or object. Integer sets a static number of app processes; object options max, spare, and idle_timeout enable dynamic management. For details, see here.

The default value is 1.

working_directoryThe app’s working directory. If not set, the Unit daemon’s working directory is used.
userUsername that runs the app process. If not set, the username configured at build time or at startup to run Unit’s non-privileged processes is used.
groupGroup name that runs the app process. If not set, the user’s primary group is used.
environmentEnvironment variables to be passed to the application.

Also, you need to set type-specific options to run the app. This Python app uses path and module:

{
    "type": "python 3.6",
    "processes": 16,
    "working_directory": "/www/python-apps",
    "path": "blog",
    "module": "blog.wsgi",
    "user": "blog",
    "group": "blog",
    "environment": {
        "DJANGO_SETTINGS_MODULE": "blog.settings.prod",
        "DB_ENGINE": "django.db.backends.postgresql",
        "DB_NAME": "blog",
        "DB_HOST": "127.0.0.1",
        "DB_PORT": "5432"
    }
}

Process Management§

Unit supports three per-app options that control the app’s processes: isolation, limits, and processes.

Process Isolation§

You can use namespace and file system isolation for your apps if Unit’s underlying OS supports them:

$ ls /proc/self/ns/

    cgroup  ipc  mnt  net  pid  ...  user  uts

The isolation application option has the following members:

OptionDescription
namespaces

Object that configures namespace isolation scheme for the application.

Available options (system-dependent; check your OS manual for guidance):

cgroupCreates a new cgroup namespace for the app.
credentialCreates a new user namespace for the app.
mountCreates a new mount namespace for the app.
networkCreates a new network namespace for the app.
pidCreates a new PID namespace for the app.
unameCreates a new UTS namespace for the app.

All options listed above are Boolean; to isolate the app, set the corresponding namespace option to true; to disable isolation, set the option to false (default).

uidmap

Array of ID mapping objects; each array item must define the following:

containerInteger that starts the user ID mapping range in the app’s namespace.
hostInteger that starts the user ID mapping range in the OS namespace.
sizeInteger size of the ID range in both namespaces.
gidmapSame as uidmap, but configures group IDs instead of user IDs.
rootfsPathname of the directory to be used as the new file system root for the app.
automount

Object that controls mount behavior if rootfs is enabled. By default, Unit automatically mounts the language runtime dependencies, a procfs at /proc/, and a tmpfs at /tmp/, but you can disable any of these default mounts:

{
    "isolation": {
        "automount": {
            "language_deps": false,
            "procfs": false,
            "tmpfs": false
        }
    }
}

Note

The uidmap and gidmap options are available only if the underlying OS supports user namespaces.

A sample isolation object that enables all namespaces and sets mappings for user and group IDs:

{
    "namespaces": {
        "cgroup": true,
        "credential": true,
        "mount": true,
        "network": true,
        "pid": true,
        "uname": true
    },

    "uidmap": [
        {
            "host": 1000,
            "container": 0,
            "size": 1000
        }
    ],

    "gidmap": [
        {
            "host": 1000,
            "container": 0,
            "size": 1000
        }
    ]
}

The rootfs option confines the app to the directory you provide, making it the new file system root. To use it, your app should have the corresponding privilege (effectively, run as root in most cases).

The root directory is changed before the language module starts the app, so any path options for the app should be relative to the new root. Note the path and home settings:

{
    "type": "python 2.7",
    "path": "/",
    "home": "/venv/",
    "module": "wsgi",
    "isolation": {
        "rootfs": "/var/app/sandbox/"
    }
}

Unit mounts language-specific files and directories to the new root so the app stays operational:

LanguageLanguage-Specific Mounts
Java
  • JVM’s libc.so directory
  • Java module’s home directory
PythonPython’s sys.path directories
Ruby
  • Ruby’s header, interpreter, and library directories: rubyarchhdrdir, rubyhdrdir, rubylibdir, rubylibprefix, sitedir, and topdir
  • Ruby’s gem installation directory (gem env gemdir)
  • Ruby’s entire gem path list (gem env gempath)

Request Limits§

The limits object controls request handling by the app process and has two integer options:

OptionDescription
timeoutRequest timeout in seconds. If an app process exceeds this limit while handling a request, Unit alerts it to cancel the request and returns an HTTP error to the client.
requestsMaximum number of requests Unit allows an app process to serve. If the limit is reached, the process is restarted; this helps to mitigate possible memory leaks or other cumulative issues.

Example:

{
    "type": "python",
    "working_directory": "/www/python-apps",
    "module": "blog.wsgi",
    "limits": {
        "timeout": 10,
        "requests": 1000
    }
}

Process Management§

The processes option offers a choice between static and dynamic process management. If you set it to an integer, Unit immediately launches the given number of app processes and keeps them without scaling.

To enable dynamic prefork model for your app, supply a processes object with the following options:

OptionDescription
max

Maximum number of application processes that Unit will maintain (busy and idle).

The default value is 1.

spareMinimum number of idle processes that Unit tries to reserve for an app. When the app is started, spare idle processes are launched; Unit assigns incoming requests to existing idle processes, forking new idles to maintain the spare level if max allows. As processes complete requests and turn idle, Unit terminates extra ones after idle_timeout.
idle_timeoutTime in seconds that Unit waits before terminating an idle process which exceeds spare.

If processes is omitted entirely, Unit creates 1 static process. If an empty object is provided: "processes": {}, dynamic behavior with default option values is assumed.

Here, Unit allows 10 processes maximum, keeps 5 idles, and terminates extra idles after 20 seconds:

{
    "max": 10,
    "spare": 5,
    "idle_timeout": 20
}

Go/Node.js§

To run your Go or Node.js applications in Unit, you need to configure them and modify their source code as suggested below. Let’s start with the app configuration; besides common options, you have the following:

OptionDescription
executable (required)

Pathname of the application, absolute or relative to working_directory.

For Node.js, supply your .js pathname and start the file itself with a proper shebang:

#!/usr/bin/env node

Note

Make sure to chmod +x the file you list here so Unit can start it.

argumentsCommand line arguments to be passed to the application. The example below is equivalent to /www/chat/bin/chat_app --tmp-files /tmp/go-cache.

Example:

{
    "type": "external",
    "working_directory": "/www/chat",
    "executable": "bin/chat_app",
    "user": "www-go",
    "group": "www-go",
    "arguments": ["--tmp-files", "/tmp/go-cache"]
}

Before applying the configuration, update the application itself.

Go§

In the import section, reference the "unit.nginx.org/go" package that you have installed or built earlier:

import (
    ...
    "unit.nginx.org/go"
    ...
)

Note

The package is required only to build the app; there’s no need to install it in the target environment.

In the main() function, replace the http.ListenandServe call with unit.ListenAndServe:

func main() {
    ...
    http.HandleFunc("/", handler)
    ...
    //http.ListenAndServe(":8080", nil)
    unit.ListenAndServe(":8080", nil)
    ...
}

The resulting application works as follows:

  • When you run it standalone, the unit.ListenAndServe call falls back to http functionality.
  • When Unit runs it, unit.ListenAndServe communicates with Unit’s router process directly, ignoring the address supplied as its first argument and relying on the listener’s settings instead.

Note

For Go-based examples, see our Grafana howto or a basic sample.

Node.js§

First, you need to have the unit-http module installed. If it’s global, symlink it in your project directory:

# npm link unit-http

Do the same if you move a Unit-hosted application to a new system where unit-http is installed globally.

Next, use unit-http instead of http in your code:

var http = require('unit-http');

Unit also supports the WebSocket protocol; your Node.js app only needs to replace the default websocket:

var webSocketServer = require('unit-http/websocket').server;

Note

For Node.js-based examples, see our Express and Docker howtos or a basic sample.

Java§

First, make sure to install Unit along with the Java language module.

Besides common options, you have the following:

OptionDescription
webapp (required)Pathname of the application’s packaged or unpackaged .war file.
classpathArray of paths to your app’s required libraries (may list directories or .jar files).
optionsArray of strings defining JVM runtime options.
threads

Integer that sets the number of worker threads per app process. When started, each app process creates a corresponding number of threads to handle requests.

The default value is 1.

thread_stack_size

Integer that defines the stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific).

The default value is system dependent and can be set with ulimit -s <SIZE_KB>.

Example:

{
    "type": "java",
    "classpath": ["/www/qwk2mart/lib/qwk2mart-2.0.0.jar"],
    "options": ["-Dlog_path=/var/log/qwk2mart.log"],
    "webapp": "/www/qwk2mart/qwk2mart.war"
}

Note

For Java-based examples, see our Jira and OpenGrok howtos or a basic sample.

Perl§

First, make sure to install Unit along with the Perl language module.

Besides common options, you have the following:

OptionDescription
script (required)PSGI script path.
threads

Integer that sets the number of worker threads per app process. When started, each app process creates a corresponding number of threads to handle requests.

The default value is 1.

thread_stack_size

Integer that defines the stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific).

The default value is system dependent and can be set with ulimit -s <SIZE_KB>.

Example:

{
    "type": "perl",
    "script": "/www/bugtracker/app.psgi",
    "working_directory": "/www/bugtracker",
    "processes": 10,
    "user": "www",
    "group": "www"
}

Note

For Perl-based examples of Perl, see our Bugzilla and Catalyst howtos or a basic sample.

PHP§

First, make sure to install Unit along with the PHP language module.

Besides common options, you have the following:

OptionDescription
root (required)Base directory of your PHP app’s file structure. All URI paths are relative to this value.
index

Filename appended to any URI paths ending with a slash; applies if script is omitted.

The default value is index.php.

optionsObject that defines the php.ini location and options.
targetsObject that defines application sections with custom root, script, and index values.
scriptFilename of a root-based PHP script that Unit uses to serve all requests to the app.

The index and script options enable two modes of operation:

  • If script is set, all requests to the application are handled by the script you provide.
  • Otherwise, the requests are served according to their URI paths; if script name is omitted, index is used.

You can customize php.ini via the options object:

OptionDescription
filePathname of the php.ini file with PHP configuration directives.
admin, userObjects for extra directives. Values in admin are set in PHP_INI_SYSTEM mode, so the app can’t alter them; user values are set in PHP_INI_USER mode and may be updated in runtime.

Directives from php.ini are overridden by settings supplied in admin and user objects.

Note

Values in options must be strings (for example, "max_file_uploads": "4", not "max_file_uploads": 4); for boolean flags, use "0" and "1" only. For details about PHP_INI_* modes, see the PHP docs.

Note

Unit implements the fastcgi_finish_request() function in a manner similar to PHP-FPM.

Example:

{
    "type": "php",
    "processes": 20,
    "root": "/www/blogs/scripts/",
    "user": "www-blogs",
    "group": "www-blogs",

    "options": {
        "file": "/etc/php.ini",
        "admin": {
            "memory_limit": "256M",
            "variables_order": "EGPCS",
            "expose_php": "0"
        },
        "user": {
            "display_errors": "0"
        }
    }
}

Targets§

You can configure up to 254 individual entry points for a single PHP application:

{
    "applications": {
        "php_app": {
            "type": "php",
            "targets": {
                "phpinfo": {
                    "script": "phpinfo.php",
                    "root": "/www/data/admin/"
                },

                "hello": {
                    "script": "hello.php",
                    "root": "/www/data/test/"
                }
            }
        }
    }
}

Each target is an object that specifies root and optionally index or script just like a common application does. Targets can be used by the pass options in listeners and routes to serve requests:

{
    "listeners": {
        "127.0.0.1:8080": {
            "pass": "applications/php_app/hello"
        },

        "127.0.0.1:80": {
            "pass": "routes"
        }
    },

    "routes": [
        {
            "match": {
                "uri": "/info"
            },

            "action": {
                "pass": "applications/php_app/phpinfo"
            }
        }
    ]
}

App-wide settings (isolation, limits, options, processes) are shared by all targets within the app.

Warning

If you specify targets, there should be no root, index, or script defined at the application level.

Note

For PHP-based examples, see our CakePHP, CodeIgniter, Drupal, WordPress, NextCloud, phpBB, Laravel, Symfony, MediaWiki, and Yii howtos or a basic sample.

Python§

First, make sure to install Unit along with the Python language module.

Besides common options, you have the following:

OptionDescription
module (required)Application module name. The module itself is imported just like in Python.
callable

Name of the callable in module that Unit uses to run the app.

The default value is application.

home

Path to the app’s virtual environment. Absolute or relative to working_directory.

Note

The Python version used to run the app depends on the type value; Unit ignores the command-line interpreter from the virtual environment for performance considerations.

pathAdditional lookup path for Python modules; this string is inserted into sys.path.
protocolA hint to instruct Unit that the app uses a certain interface; can be asgi or wsgi.
threads

Integer that sets the number of worker threads per app process. When started, each app process creates a corresponding number of threads to handle requests.

The default value is 1.

thread_stack_size

Integer that defines the stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific).

The default value is system dependent and can be set with ulimit -s <SIZE_KB>.

Example:

{
    "type": "python",
    "processes": 10,
    "working_directory": "/www/store/",
    "path": "/www/store/cart/",
    "home": "/www/store/.virtualenv/",
    "module": "wsgi",
    "callable": "app",
    "user": "www",
    "group": "www"
}

You can provide the callable in two forms. The first one uses WSGI (PEP 333 or PEP 3333):

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    yield b'Hello, WSGI\n'

The second one, supported for Python 3.5+, uses ASGI:

async def application(scope, receive, send):

    await send({
        'type': 'http.response.start',
        'status': 200
    })

    await send({
        'type': 'http.response.body',
        'body': b'Hello, ASGI\n'
    })

Note

Legacy two-callable ASGI 2.0 applications were not supported prior to Unit 1.21.0.

Choose either one according to your needs; Unit will attempt to infer your choice automatically. If automatic inference fails, use the protocol option to name the interface explicitly.

Note

For Python-based examples, see our Datasette, Django, Django Channels, FastAPI, Flask, Guillotina, Mercurial, MoinMoin, Pyramid, Quart, Responder, Review Board, Sanic, Starlette, and Trac howtos or a basic sample.

Ruby§

First, make sure to install Unit along with the Ruby language module.

Note

Unit uses the Rack interface to run Ruby scripts; you need to have it installed as well:

$ gem install rack

Besides common options, you have the following:

OptionDescription
script (required)Rack script pathname, including the .ru extension: /www/rubyapp/script.ru.
threads

Integer that sets the number of worker threads per app process. When started, each app process creates a corresponding number of threads to handle requests.

The default value is 1.

Example:

{
    "type": "ruby",
    "processes": 5,
    "user": "www",
    "group": "www",
    "script": "/www/cms/config.ru"
}

Note

For Ruby-based examples, see our Redmine howto or a basic sample.

Settings§

Unit has a global settings configuration object that stores instance-wide preferences. Its http option fine-tunes the handling of HTTP requests from the clients:

OptionDescription
header_read_timeout

Maximum number of seconds to read the header of a client’s request. If Unit doesn’t receive the entire header from the client within this interval, it responds with a 408 Request Timeout error.

The default value is 30.

body_read_timeout

Maximum number of seconds to read data from the body of a client’s request. It limits the interval between consecutive read operations, not the time to read the entire body. If Unit doesn’t receive any data from the client within this interval, it responds with a 408 Request Timeout error.

The default value is 30.

send_timeout

Maximum number of seconds to transmit data in the response to a client. It limits the interval between consecutive transmissions, not the entire response transmission. If the client doesn’t receive any data within this interval, Unit closes the connection.

The default value is 30.

idle_timeout

Maximum number of seconds between requests in a keep-alive connection. If no new requests arrive within this interval, Unit responds with a 408 Request Timeout error and closes the connection.

The default value is 180.

max_body_size

Maximum number of bytes in the body of a client’s request. If the body size exceeds this value, Unit responds with a 413 Payload Too Large error and closes the connection.

The default value is 8388608 (8 MB).

staticObject that configures static asset handling, containing a single object named mime_types. In turn, mime_types defines specific MIME types as options. An option’s value can be a string or an array of strings; each string must specify a filename extension or a specific filename that is included in the MIME type.
discard_unsafe_fields

Controls the parsing mode of header field names. If set to true, Unit only processes headers with names consisting of alphanumeric characters and hyphens (-); otherwise, all valid RFC 7230 header fields are processed.

The default value is true.

Example:

{
    "settings": {
        "http": {
            "header_read_timeout": 10,
            "body_read_timeout": 10,
            "send_timeout": 10,
            "idle_timeout": 120,
            "max_body_size": 6291456,
            "static": {
                "mime_types": {
                    "text/plain": [
                         ".log",
                         "README",
                         "CHANGES"
                    ]
                }
            },
            "discard_unsafe_fields": false
        }
    }
}

Note

Built-in support for MIME types includes .aac, .apng, .atom, .avi, .avif, avifs, .bin, .css, .deb, .dll, .exe, .flac, .gif, .htm, .html, .ico, .img, .iso, .jpeg, .jpg, .js, .json, .md, .mid, .midi, .mp3, .mp4, .mpeg, .mpg, .msi, .ogg, .otf, .pdf, .png, .rpm, .rss, .rst, .svg, .ttf, .txt, .wav, .webm, .webp, .woff2, .woff, .xml, and .zip. Built-ins can be overridden, and new types can be added:

# curl -X PUT -d '{"text/x-code": [".c", ".h"]}' /path/to/control.unit.sock \
       http://localhost/config/settings/http/static/mime_types
{
       "success": "Reconfiguration done."
}

Access Log§

To enable access logging, specify the log file path in the access_log option of the config object.

In the example below, all requests will be logged to /var/log/access.log:

# curl -X PUT -d '"/var/log/access.log"' \
       --unix-socket /path/to/control.unit.sock \
       http://localhost/config/access_log

    {
        "success": "Reconfiguration done."
    }

The log is written in the Combined Log Format. Example of a log line:

127.0.0.1 - - [21/Oct/2015:16:29:00 -0700] "GET / HTTP/1.1" 200 6022 "http://example.com/links.html" "Godzilla/5.0 (X11; Minix i286) Firefox/42"

SSL/TLS and Certificates§

To set up SSL/TLS access for your application, upload a .pem file containing your certificate chain and private key to Unit. Next, reference the uploaded bundle in the listener’s configuration. After that, the listener’s application becomes accessible via SSL/TLS.

Note

For the details of certificate issuance and renewal in Unit, see an example in TLS with Certbot.

First, create a .pem file with your certificate chain and private key:

$ cat cert.pem ca.pem key.pem > bundle.pem

Note

Usually, your website’s certificate (optionally followed by the intermediate CA certificate) is enough to build a certificate chain. If you add more certificates to your chain, order them leaf to root.

Upload the resulting file to Unit’s certificate storage under a suitable name:

# curl -X PUT --data-binary @bundle.pem --unix-socket \
       /path/to/control.unit.sock http://localhost/certificates/<bundle>

    {
        "success": "Certificate chain uploaded."
    }

Warning

Don’t use -d for file upload; this option damages .pem files. Use the --data-binary option when uploading file-based data with curl to avoid data corruption.

Internally, Unit stores uploaded certificate bundles along with other configuration data in its state subdirectory; Unit’s control API maps them to a separate configuration section, aptly named certificates:

{
    "certificates": {
        "<bundle>": {
            "key": "RSA (4096 bits)",
            "chain": [
                {
                    "subject": {
                        "common_name": "example.com",
                        "alt_names": [
                            "example.com",
                            "www.example.com"
                        ],

                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme, Inc."
                    },

                    "issuer": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "validity": {
                        "since": "Sep 18 19:46:19 2018 GMT",
                        "until": "Jun 15 19:46:19 2021 GMT"
                    }
                },

                {
                    "subject": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "issuer": {
                        "common_name": "root.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Root Certification Authority"
                    },

                    "validity": {
                        "since": "Feb 22 22:45:55 2016 GMT",
                        "until": "Feb 21 22:45:55 2019 GMT"
                    }
                }
            ]
        }
    }
}

Note

You can access individual certificates in your chain, as well as specific alternative names, by their indexes:

# curl -X GET --unix-socket /path/to/control.unit.sock \
       http://localhost/certificates/<bundle>/chain/0/
# curl -X GET --unix-socket /path/to/control.unit.sock \
       http://localhost/certificates/<bundle>/chain/0/subject/alt_names/0/

Next, add a tls object to the listener configuration, referencing the uploaded bundle in certificate:

{
    "listeners": {
        "127.0.0.1:443": {
            "pass": "applications/wsgi-app",
            "tls": {
                "certificate": "<bundle>"
            }
        }
    }
}

The resulting control API configuration may look like this:

{
    "certificates": {
        "<bundle>": {
            "key": "<key type>",
            "chain": ["<certificate chain, omitted for brevity>"]
        }
    },

    "config": {
        "listeners": {
            "127.0.0.1:443": {
                "pass": "applications/wsgi-app",
                "tls": {
                    "certificate": "<bundle>"
                }
            }
        },

        "applications": {
            "wsgi-app": {
                "type": "python",
                "module": "wsgi",
                "path": "/usr/www/wsgi-app/"
            }
        }
    }
}

Now you’re solid. The application is accessible via SSL/TLS:

$ curl -v https://127.0.0.1
    ...
    * TLSv1.2 (OUT), TLS handshake, Client hello (1):
    * TLSv1.2 (IN), TLS handshake, Server hello (2):
    * TLSv1.2 (IN), TLS handshake, Certificate (11):
    * TLSv1.2 (IN), TLS handshake, Server finished (14):
    * TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
    * TLSv1.2 (OUT), TLS change cipher, Client hello (1):
    * TLSv1.2 (OUT), TLS handshake, Finished (20):
    * TLSv1.2 (IN), TLS change cipher, Client hello (1):
    * TLSv1.2 (IN), TLS handshake, Finished (20):
    * SSL connection using TLSv1.2 / AES256-GCM-SHA384
    ...

Finally, you can DELETE a certificate bundle that you don’t need anymore from the storage:

# curl -X DELETE --unix-socket /path/to/control.unit.sock \
       http://localhost/certificates/<bundle>

    {
        "success": "Certificate deleted."
    }

Note

You can’t delete certificate bundles still referenced in your configuration, overwrite existing bundles using PUT, or (obviously) delete non-existent ones.

Happy SSLing!

Full Example§

{
    "certificates": {
        "bundle": {
            "key": "RSA (4096 bits)",
            "chain": [
                {
                    "subject": {
                        "common_name": "example.com",
                        "alt_names": [
                            "example.com",
                            "www.example.com"
                        ],

                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme, Inc."
                    },

                    "issuer": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "validity": {
                        "since": "Sep 18 19:46:19 2018 GMT",
                        "until": "Jun 15 19:46:19 2021 GMT"
                    }
                },

                {
                    "subject": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "issuer": {
                        "common_name": "root.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Root Certification Authority"
                    },

                    "validity": {
                        "since": "Feb 22 22:45:55 2016 GMT",
                        "until": "Feb 21 22:45:55 2019 GMT"
                    }
                }
            ]
        }
    },

    "config": {
        "settings": {
            "http": {
                "header_read_timeout": 10,
                "body_read_timeout": 10,
                "send_timeout": 10,
                "idle_timeout": 120,
                "max_body_size": 6291456,
                "static": {
                    "mime_types": {
                        "text/plain": [
                             ".log",
                             "README",
                             "CHANGES"
                        ]
                    }
                },
                "discard_unsafe_fields": false
            }
        },

        "listeners": {
            "*:8000": {
                "pass": "routes",
                "tls": {
                    "certificate": "bundle"
                }
            },

            "127.0.0.1:8001": {
                "pass": "applications/drive"
            },

            "*:8080": {
                "pass": "upstreams/rr-lb"
            }
        },

        "routes": [
            {
                "match": {
                    "uri": "/admin/*",
                    "scheme": "https",
                    "arguments": {
                        "mode": "strict",
                        "access": "!raw"
                    },

                    "cookies": {
                        "user_role": "admin"
                    }
                },

                "action": {
                    "pass": "applications/cms"
                }
            },
            {
                "match": {
                    "host": "admin.emea-*.*.example.com",
                    "source": "*:8000-9000"
                },

                "action": {
                    "pass": "applications/blogs/admin"
                }
            },
            {
                "match": {
                    "host": ["blog.example.com", "blog.*.org"],
                    "source": "*:8000-9000"
                },

                "action": {
                    "pass": "applications/blogs/core"
                }
            },
            {
                "match": {
                    "host": "example.com",
                    "source": "127.0.0.0-127.0.0.255:8080-8090",
                    "uri": "/chat/*"
                },

                "action": {
                    "pass": "applications/chat"
                }
            },
            {
                "match": {
                    "host": "example.com",
                    "source": [
                        "10.0.0.0/7:1000",
                        "10.0.0.0/32:8080-8090"
                    ]
                },

                "action": {
                    "pass": "applications/store"
                }
            },
            {
                "match": {
                    "host": "wiki.example.com"
                },

                "action": {
                    "pass": "applications/wiki"
                }
            },
            {
                "match": {
                     "uri": "/legacy/*"
                },

                "action": {
                    "return": 301,
                    "location": "https://legacy.example.com"
                }
            },
            {
                "match": {
                    "scheme": "http"
                },

                "action": {
                    "proxy": "http://127.0.0.1:8080"
                }
            },
            {
                "action": {
                    "share": "/www/static/",
                    "fallback": {
                        "proxy": "http://127.0.0.1:9000"
                    }
                }
            }
        ],

        "applications": {
            "blogs": {
                "type": "php",
                "targets": {
                    "admin": {
                        "root": "/www/blogs/admin/",
                        "script": "index.php"
                    },

                    "core" : {
                        "root": "/www/blogs/scripts/"
                    }
                },

                "limits": {
                    "timeout": 10,
                    "requests": 1000
                },

                "options": {
                    "file": "/etc/php.ini",
                    "admin": {
                        "memory_limit": "256M",
                        "variables_order": "EGPCS",
                        "expose_php": "0"
                    },

                    "user": {
                        "display_errors": "0"
                    }
                },

                "processes": 4
            },

            "chat": {
                "type": "external",
                "executable": "bin/chat_app",
                "group": "www-chat",
                "user": "www-chat",
                "working_directory": "/www/chat/",
                "isolation": {
                    "namespaces": {
                        "cgroup": false,
                        "credential": true,
                        "mount": false,
                        "network": false,
                        "pid": false,
                        "uname": false
                    },

                    "uidmap": [
                        {
                            "host": 1000,
                            "container": 0,
                            "size": 1000
                        }
                    ],

                    "gidmap": [
                        {
                            "host": 1000,
                            "container": 0,
                            "size": 1000
                        }
                    ],

                    "automount": {
                        "language_deps": false,
                        "procfs": false,
                        "tmpfs": false
                    }
                }
            },

            "cms": {
                "type": "ruby",
                "script": "/www/cms/main.ru",
                "working_directory": "/www/cms/"
            },

            "drive": {
                "type": "perl",
                "script": "app.psgi",
                "threads": 2,
                "thread_stack_size": 4096,
                "working_directory": "/www/drive/",
                "processes": {
                    "max": 10,
                    "spare": 5,
                    "idle_timeout": 20
                }
            },

            "store": {
                "type": "java",
                "webapp": "/www/store/store.war",
                "classpath": ["/www/store/lib/store-2.0.0.jar"],
                "options": ["-Dlog_path=/var/log/store.log"]
            },

            "wiki": {
                "type": "python",
                "module": "asgi",
                "protocol": "asgi",
                "callable": "app",
                "environment": {
                    "DJANGO_SETTINGS_MODULE": "wiki.settings.prod",
                    "DB_ENGINE": "django.db.backends.postgresql",
                    "DB_NAME": "wiki",
                    "DB_HOST": "127.0.0.1",
                    "DB_PORT": "5432"
                },

                "path": "/www/wiki/",
                "processes": 10
            }
        },

        "upstreams": {
            "rr-lb": {
                "servers": {
                    "192.168.1.100:8080": { },
                    "192.168.1.101:8080": {
                        "weight": 2
                    }
                }
            }
        },

        "access_log": "/var/log/access.log"
    }
}