Configuration§
The /config
section of the
control API
handles Unit’s general configuration with entities such as
listeners,
routes,
applications,
or
upstreams.
Listeners§
To accept requests,
add a listener object in the config/listeners
API section;
the object’s name can be:
- A unique IP socket:
127.0.0.1:80
,[::1]:8080
- A wildcard that matches any host IPs on the port:
*:80
- On Linux-based systems,
abstract UNIX sockets
can be used as well:
unix:@abstract_socket
.
Note
Also on Linux-based systems,
wildcard listeners can’t overlap with other listeners
on the same port
due to rules imposed by the kernel.
For example, *:8080
conflicts with 127.0.0.1:8080
;
in particular,
this means *:8080
can’t be immediately replaced
by 127.0.0.1:8080
(or vice versa)
without deleting it first.
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:
Option | Description |
---|---|
pass (required) |
Destination to which the listener passes incoming requests. Possible alternatives:
The value is variable -interpolated; if it matches no configuration entities after interpolation, a 404 “Not Found” response is returned. |
forwarded |
Object; configures client IP address and protocol replacement. |
tls |
Object; defines SSL/TLS settings. |
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
relays requests at any host IPs
to the main
route:
{
"127.0.0.1:8300": {
"pass": "applications/blogs$uri"
},
"*:8400": {
"pass": "routes/main"
}
}
Also, 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": []
}
}
SSL/TLS Configuration§
The tls
object provides the following options:
Option | Description |
---|---|
certificate (required) |
String or an array of strings; refers to one or more certificate bundles uploaded earlier, enabling secure communication via the listener. |
conf_commands |
Object; defines the OpenSSL configuration commands to be set for the listener. To have this option, Unit must be built and run with OpenSSL 1.0.2+: $ openssl version
OpenSSL 1.1.1d 10 Sep 2019
Also, make sure your OpenSSL version supports the commands set by this option. |
session |
Object; configures the TLS session cache and tickets for the listener. |
To use a certificate bundle you
uploaded
earlier,
name it in the certificate
option of the tls
object:
{
"listeners": {
"127.0.0.1:443": {
"pass": "applications/wsgi-app",
"tls": {
"certificate": "bundle"
}
}
}
}
Configuring Multiple Bundles
Since version 1.23.0,
Unit supports configuring
Server Name Indication (SNI)
on a listener
by supplying an array of certificate bundle names
for the certificate
option value:
{
"*:443": {
"pass": "routes",
"tls": {
"certificate": [
"bundleA",
"bundleB",
"bundleC"
]
}
}
}
- If the connecting client sends a server name, Unit responds with the matching certificate bundle.
- If the name matches several bundles, exact matches have priority over wildcards; if this doesn’t help, the one listed first is used.
- If there’s no match or no server name was sent, Unit uses the first bundle on the list.
To set custom OpenSSL
configuration commands
for a listener,
use the conf_commands
object in tls
:
{
"tls": {
"certificate": "bundle",
"conf_commands": {
"ciphersuites": "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256",
"minprotocol": "TLSv1.3"
}
}
}
The session
object in tls
configures the session settings of the listener:
Option | Description |
---|---|
cache_size |
Integer; sets the number of sessions in the TLS session cache. The default is |
tickets |
Boolean, string, or an array of strings; configures TLS session tickets. The default is |
timeout |
Integer; sets the session timeout for the TLS session cache. When a new session is created,
its lifetime derives from current time and The default is |
Example:
{
"tls": {
"certificate": "bundle",
"session": {
"cache_size": 10240,
"timeout": 60,
"tickets": [
"k5qMHi7IMC7ktrPY3lZ+sL0Zm8oC0yz6re+y/zCj0H0/sGZ7yPBwGcb77i5vw6vCx8vsQDyuvmFb6PZbf03Auj/cs5IHDTYkKIcfbwz6zSU=",
"3Cy+xMFsCjAek3TvXQNmCyfXCnFNAcAOyH5xtEaxvrvyyCS8PJnjOiq2t4Rtf/Gq",
"8dUI0x3LRnxfN0miaYla46LFslJJiBDNdFiPJdqr37mYQVIzOWr+ROhyb1hpmg/QCM2qkIEWJfrJX3I+rwm0t0p4EGdEVOXQj7Z8vHFcbiA="
]
}
}
}
The tickets
option works as follows:
Boolean values enable or disable session tickets; with
true
, a random session ticket key is used:{ "session": { "tickets": true } }
A string enables tickets and explicitly sets the session ticket key:
{ "session": { "tickets": "IAMkP16P8OBuqsijSDGKTpmxrzfFNPP4EdRovXH2mqstXsodPC6MqIce5NlMzHLP" } }
This enables ticket reuse in scenarios where the key is shared between individual servers.
Shared Key Rotation
If multiple Unit instances need to recognize tickets issued by each other (for example, when running behind a load balancer), they should share session ticket keys.
For example, consider three SSH-enabled servers named
unit*.example.com
, with Unit installed and identical*:443
listeners configured. To configure a single set of three initial keys on each server:SERVERS="unit1.example.com unit2.example.com unit3.example.com" KEY1=$(openssl rand -base64 48) KEY2=$(openssl rand -base64 48) KEY3=$(openssl rand -base64 48) for SRV in $SERVERS; do ssh root@$SRV \ curl -X PUT -d '["$KEY1", "$KEY2", "$KEY3"]' --unix-socket /path/to/control.unit.sock \ 'http://localhost/config/listeners/*:443/tls/session/tickets/' done
To add a new key on each server:
NEWKEY=$(openssl rand -base64 48) for SRV in $SERVERS; do ssh root@$SRV \ curl -X POST -d '\"$NEWKEY\"' --unix-socket /path/to/control.unit.sock \ 'http://localhost/config/listeners/*:443/tls/session/tickets/'" done
To delete the oldest key after adding the new one:
for SRV in $SERVERS; do ssh root@$SRV \ curl -X DELETE --unix-socket /path/to/control.unit.sock \ 'http://localhost/config/listeners/*:443/tls/session/tickets/0' done
This scheme enables safely sharing session ticket keys between individual Unit instances.
Unit supports AES256 (80-byte keys) or AES128 (48-byte keys); the bytes should be encoded in Base64:
$ openssl rand -base64 48 LoYjFVxpUFFOj4TzGkr5MsSIRMjhuh8RCsVvtIJiQ12FGhn0nhvvQsEND1+OugQ7 $ openssl rand -base64 80 GQczhdXawyhTrWrtOXI7l3YYUY98PrFYzjGhBbiQsAWgaxm+mbkm4MmZZpDw0tkK YTqYWxofDtDC4VBznbBwTJTCgYkJXknJc4Gk2zqD1YA=
An array of strings just like the one above:
{ "session": { "tickets": [ "IAMkP16P8OBuqsijSDGKTpmxrzfFNPP4EdRovXH2mqstXsodPC6MqIce5NlMzHLP", "Ax4bv/JvMWoQG+BfH0feeM9Qb32wSaVVKOj1+1hmyU8ORMPHnf3Tio8gLkqm2ifC" ] } }
Unit uses these keys to decrypt the tickets submitted by clients who want to recover their session state; the last key is always used to create new session tickets and update the tickets created earlier.
Note
An empty array effectively disables session tickets, same as setting
tickets
tofalse
.
IP, Protocol Forwarding§
Unit enables the X-Forwarded-*
header fields
with the forwarded
object and its options:
Option | Description |
---|---|
source (required) |
String or an array of strings; defines address-based patterns for trusted addresses. Replacement occurs only if the source IP of the request is a match. A special case here is the |
client_ip |
String; names the HTTP header fields to expect in the request. They should use the X-Forwarded-For format where the value is a comma- or space-separated list of IPv4s or IPv6s. |
protocol |
String;
defines the relevant HTTP header field to look for in the request.
Unit expects it to follow the
X-Forwarded-Proto
notation,
with the field value itself
being http , https , or on . |
recursive |
Boolean;
controls how the The default is |
Note
Besides source
,
the forwarded
object must specify
client_ip
, protocol
, or both.
Warning
Before version 1.28.0,
Unit provided the client_ip
object
that evolved into forwarded
:
client_ip (pre-1.28.0) |
forwarded (post-1.28.0) |
---|---|
header |
client_ip |
recursive |
recursive |
source |
source |
N/A | protocol |
This old syntax still works but will be eventually deprecated, though not earlier than version 1.30.0.
When forwarded
is set,
Unit respects the appropriate header fields
only if the immediate source IP of the request
matches
the source
option.
Mind that it can use not only subnets but any
address-based patterns:
{
"forwarded": {
"client_ip": "X-Forwarded-For",
"source": [
"198.51.100.1-198.51.100.254",
"!198.51.100.128/26",
"203.0.113.195"
]
}
}
Overwriting Protocol Scheme§
The protocol
option enables overwriting
the incoming request’s protocol scheme
based on the header field it specifies.
Consider the following forwarded
configuration:
{
"forwarded": {
"protocol": "X-Forwarded-Proto",
"source": [
"192.0.2.0/24",
"198.51.100.0/24"
]
}
}
Suppose a request arrives with the following header field:
X-Forwarded-Proto: https
If the source IP of the request matches source
,
Unit handles this request as an https
one.
Originating IP Identification§
Unit also supports identifying the clients’ originating IPs
with the client_ip
option:
{
"forwarded": {
"client_ip": "X-Forwarded-For",
"recursive": false,
"source": [
"192.0.2.0/24",
"198.51.100.0/24"
]
}
}
Suppose a request arrives with the following header fields:
X-Forwarded-For: 192.0.2.18
X-Forwarded-For: 203.0.113.195, 198.51.100.178
If recursive
is set to false
(default),
Unit chooses the rightmost address of the last field
named in client_ip
as the originating IP of the request.
In the example,
it’s set to 198.51.100.178 for requests from 192.0.2.0/24 or 198.51.100.0/24.
If recursive
is set to true
,
Unit inspects all client_ip
fields in reverse order.
Each is traversed from right to left
until the first non-trusted address;
if found, it’s chosen as the originating IP.
In the previous example with "recursive": true
,
the client IP would be set to 203.0.113.195
because 198.51.100.178 is also trusted;
this simplifies working behind multiple reverse proxies.
Routes§
The config/routes
configuration entity
defines internal request routing.
It receives requests
from listeners
and filters 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
is an array
that defines a single route:
{
"listeners": {
"*:8300": {
"pass": "routes"
}
},
"routes": [
"..."
]
}
Another form is an object with one or more named route arrays as members:
{
"listeners": {
"*:8300": {
"pass": "routes/main"
}
},
"routes": {
"main": [
"..."
],
"route66": [
"..."
]
}
}
Route Steps§
A route array contains step objects as elements; they accept the following options:
Option | Description |
---|---|
action (required) |
Object; defines how matching requests are handled. |
match |
Object; defines the step’s conditions to be matched. |
A request passed to a route traverses its steps sequentially:
- If all
match
conditions in a step are met, the traversal ends and the step’saction
is performed. - If a step’s condition isn’t met, Unit proceeds to the next step of the route.
- If no steps of the route match, a 404 “Not Found” response is returned.
Warning
If a step omits the match
option,
its action
occurs automatically.
Thus, use no more than one such step per route,
always placing it last to avoid potential routing issues.
Ad-Hoc Examples
A basic one:
{
"routes": [
{
"match": {
"host": "example.com",
"scheme": "https",
"uri": "/php/*"
},
"action": {
"pass": "applications/php_version"
}
},
{
"action": {
"share": "/www/static_version$uri"
}
}
]
}
This route passes all HTTPS requests
to the /php/
subsection of the example.com
website
to the php_version
app.
All other requests are served with static content
from the /www/static_version/
directory.
If there’s no matching content,
a 404 “Not Found” response is returned.
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$uri"
}
}
],
"http_site": [
{
"match": {
"uri": "/v2_site/*"
},
"action": {
"pass": "applications/v2_site"
}
},
{
"action": {
"proxy": "http://127.0.0.1:9000"
}
}
]
}
}
Here, a route called main
is explicitly defined,
so routes
is an object instead of an array.
The first step of the route passes all HTTP requests
to the http_site
app.
The second step passes all requests
that target blog.example.com
to the blog
app.
The final step serves requests for certain file types
from the /www/static/
directory.
If no steps match,
a 404 “Not Found” response is returned.
Matching Conditions§
Conditions in a
route step’s
match
object
define patterns to be compared to the request’s properties:
Property | Patterns Are Matched Against | Case‑ Sensitive |
---|---|---|
arguments |
Arguments supplied with the request’s
query string;
these names and value pairs are
percent decoded,
with plus signs
(+ )
replaced by spaces. |
Yes |
cookies |
Cookies supplied with the request. | Yes |
destination |
Target IP address and optional port of the request. | No |
headers |
Header fields supplied with the request. | No |
host |
Host
header field,
converted to lower case and normalized
by removing the port number and the trailing period
(if any). |
No |
method |
Method from the request line, uppercased. | No |
query |
Query string,
percent decoded,
with plus signs
(+ )
replaced by spaces. |
Yes |
scheme |
URI
scheme.
Accepts only two patterns,
either http or https . |
No |
source |
Source IP address and optional port of the request. | No |
uri |
Request target, percent decoded and normalized by removing the query string and resolving relative references (“.” and “..”, “//”). | Yes |
Arguments vs. Query
Both arguments
and query
operate on the query string,
but query
is matched against the entire string
whereas arguments
considers only the key-value pairs
such as key1=4861&key2=a4f3
.
Use arguments
to define conditions
based on key-value pairs in the query string:
"arguments": {
"key1": "4861",
"key2": "a4f3"
}
Argument order is irrelevant:
key1=4861&key2=a4f3
and key2=a4f3&key1=4861
are considered the same.
Also, multiple occurrences of an argument must all match,
so key=4861&key=a4f3
matches this:
"arguments":{
"key": "*"
}
But not this:
"arguments":{
"key": "a*"
}
To the contrary,
use query
if your conditions concern query strings
but don’t rely on key-value pairs:
"query": [
"utf8",
"utf16"
]
This only matches query strings
of the form
https://example.com?utf8
or https://example.com?utf16
.
Match Resolution§
To be a match, the property must meet two requirements:
- If there are patterns without negation
(the
!
prefix), at least one of them matches the property value. - No negated patterns match the property value.
Formal Explanation
This logic can be described 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 is:
Here, the URI of the request must fit pattern3
,
but must not match pattern1
or pattern2
:
{
"match": {
"uri": [
"!pattern1",
"!pattern2",
"pattern3"
]
},
"action": {
"pass": "..."
}
}
Additionally, special matching logic applies to
arguments
, cookies
, and headers
.
Each of these can be either
a single object that lists custom-named properties and their patterns
or an array of such objects.
To match a single object,
the request must match all properties named in the object.
To match an object array,
it’s enough to match any single one of its item objects.
The following condition matches only
if the request arguments include arg1
and arg2
,
and both match their patterns:
{
"match": {
"arguments": {
"arg1": "pattern",
"arg2": "pattern"
}
},
"action": {
"pass": "..."
}
}
With an object array,
the condition matches
if the request’s arguments include
arg1
or arg2
(or both)
that matches the respective pattern:
{
"match": {
"arguments": [
{
"arg1": "pattern"
},
{
"arg2": "pattern"
}
]
},
"action": {
"pass": "..."
}
}
The following example combines all matching types.
Here, host
, method
, uri
,
arg1
and arg2
,
either cookie1
or cookie2
,
and either header1
or header2
and header3
must be matched
for the action
to be taken
(host & method & uri & arg1 & arg2 & (cookie1 | cookie2)
& (header1 | (header2 & header3))
):
{
"match": {
"host": "pattern",
"method": "!pattern",
"uri": [
"pattern",
"!pattern"
],
"arguments": {
"arg1": "pattern",
"arg2": "!pattern"
},
"cookies": [
{
"cookie1": "pattern",
},
{
"cookie2": "pattern",
}
],
"headers": [
{
"header1": "pattern",
},
{
"header2": "pattern",
"header3": "pattern"
}
]
},
"action": {
"pass": "..."
}
}
Object Pattern Examples
This requires mode=strict
and any access
argument other than access=full
in the URI query:
{
"match": {
"arguments": {
"mode": "strict",
"access": "!full"
}
},
"action": {
"pass": "..."
}
}
This matches requests that
either use gzip
and identify as Mozilla/5.0
or list curl
as the user agent:
{
"match": {
"headers": [
{
"Accept-Encoding": "*gzip*",
"User-Agent": "Mozilla/5.0*"
},
{
"User-Agent": "curl*"
}
]
},
"action": {
"pass": "..."
}
}
Pattern Syntax§
Individual patterns can be
address-based
(source
and destination
)
or string-based
(other properties).
String-based patterns must match the property to a character; wildcards or regexes modify this behavior:
- A wildcard pattern may contain any combination of wildcards
(
*
), each standing for an arbitrary number of characters:How*s*that*to*you
.
- A regex pattern starts with a tilde
(
~
):~^\d+\.\d+\.\d+\.\d+
(escaping backslashes is a JSON requirement). The regexes are PCRE-flavored.
Percent Encoding In Arguments, Query, and URI Patterns
Argument names, non-regex string patterns in arguments
,
query
, and uri
can be
percent encoded
to mask special characters
(!
is %21
, ~
is %7E
,
*
is %2A
, %
is %25
)
or even target single bytes.
For example, you can select diacritics such as Ö or Å
by their starting byte 0xC3
in UTF-8:
{
"match": {
"arguments": {
"word": "*%C3*"
}
},
"action": {
"pass": "..."
}
}
Unit decodes such strings and matches them against respective request entities, decoding these as well:
{
"routes": [
{
"match": {
"query": "%7Efuzzy word search"
},
"action": {
"return": 200
}
}
]
}
This condition matches the following percent-encoded request:
$ curl http://127.0.0.1/?~fuzzy%20word%20search -v
> GET /?~fuzzy%20word%20search HTTP/1.1
...
< HTTP/1.1 200 OK
...
Note that the encoded spaces
(%20
)
in the request
match their unencoded counterparts in the pattern;
vice versa, the encoded tilde
(%7E
)
in the condition matches ~
in the request.
String Pattern Examples
A regular expression that matches any .php
files
in the /data/www/
directory and its subdirectories.
Note the backslashes;
escaping is a JSON-specific requirement:
{
"match": {
"uri": "~^/data/www/.*\\.php(/.*)?$"
},
"action": {
"pass": "..."
}
}
Only subdomains of example.com
match:
{
"match": {
"host": "*.example.com"
},
"action": {
"pass": "..."
}
}
Only requests for .php
files
located in /admin/
’s subdirectories
match:
{
"match": {
"uri": "/admin/*/*.php"
},
"action": {
"pass": "..."
}
}
Here, any eu-
subdomains of example.com
match
except eu-5.example.com
:
{
"match": {
"host": [
"eu-*.example.com",
"!eu-5.example.com"
]
},
"action": {
"pass": "..."
}
}
Any methods match
except HEAD
and GET
:
{
"match": {
"method": [
"!HEAD",
"!GET"
]
},
"action": {
"pass": "..."
}
}
You can also combine certain special characters in a pattern.
Here, any URIs match
except the ones containing /api/
:
{
"match": {
"uri": "!*/api/*"
},
"action": {
"pass": "..."
}
}
Here, URIs of any articles
that don’t look like YYYY-MM-DD
dates
match.
Again, note the backslashes;
they are a JSON requirement:
{
"match": {
"uri": [
"/articles/*",
"!~/articles/\\d{4}-\\d{2}-\\d{2}"
]
},
"action": {
"pass": "..."
}
}
Address-based patterns define individual IPv4 (dot-decimal or CIDR), IPv6 (hexadecimal or CIDR), or any UNIX domain socket addresses that must exactly match the property; wildcards and ranges modify this behavior:
- Wildcards
(
*
) can only match arbitrary IPs (*:<port>
). - Ranges
(
-
) work with both IPs (in respective notation) and ports (<start_port>-<end_port>
).
Address-Based Allow-Deny Lists
Addresses come in handy when implementing an allow-deny mechanism with routes, for instance:
"routes": [
{
"match": {
"source": [
"!192.168.1.1",
"!10.1.1.0/16",
"192.168.1.0/24",
"2001:0db8::/32"
]
},
"action": {
"share": "/www/data$uri"
}
}
]
See here for details of pattern resolution order; this corresponds to the following nginx directive:
location / {
deny 10.1.1.0/16;
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 2001:0db8::/32;
deny all;
root /www/data;
}
Address Pattern Examples
This uses IPv4-based matching with wildcards and ranges:
{
"match": {
"source": [
"192.0.2.1-192.0.2.200",
"198.51.100.1-198.51.100.200:8000",
"203.0.113.1-203.0.113.200:8080-8090",
"*:80"
],
"destination": [
"192.0.2.0/24",
"198.51.100.0/24:8000",
"203.0.113.0/24:8080-8090",
"*:80"
]
},
"action": {
"pass": "..."
}
}
This uses IPv6-based matching with wildcards and ranges:
{
"match": {
"source": [
"2001:0db8::-2001:0db8:aaa9:ffff:ffff:ffff:ffff:ffff",
"[2001:0db8:aaaa::-2001:0db8:bbbb::]:8000",
"[2001:0db8:bbbb::1-2001:0db8:cccc::]:8080-8090",
"*:80"
],
"destination": [
"2001:0db8:cccd::/48",
"[2001:0db8:ccce::/48]:8000",
"[2001:0db8:ccce:ffff::/64]:8080-8090",
"*:80"
]
},
"action": {
"pass": "..."
}
}
This matches any of the listed IPv4 or IPv6 addresses:
{
"match": {
"destination": [
"127.0.0.1",
"192.168.0.1",
"::1",
"2001:0db8:1::c0a8:1"
]
},
"action": {
"pass": "..."
}
}
Here, any IPs from the range match
except 192.0.2.9
:
{
"match": {
"source": [
"192.0.2.1-192.0.2.10",
"!192.0.2.9"
]
},
"action": {
"pass": "..."
}
}
This matches any IPs but limits the acceptable ports:
{
"match": {
"source": [
"*:80",
"*:443",
"*:8000-8080"
]
},
"action": {
"pass": "..."
}
}
This matches any UNIX domain sockets:
{
"match": {
"source": "unix"
},
"action": {
"pass": "..."
}
}
Handling Actions§
If a request matches all
conditions
of a route step
or the step itself omits the match
object,
Unit handles the request with the respective action
.
The mutually exclusive action
types are:
Option | Description | Details |
---|---|---|
pass |
Destination for the request,
identical to a listener’s pass option. |
Listeners |
proxy |
Socket address of an HTTP server to where the request is proxied. | Proxying |
return |
HTTP status code with a context-dependent redirect location. | Instant Responses, Redirects |
share |
File paths that serve the request with static content. | Static Files |
An additional option is applicable to any of these actions:
Option | Description | Details |
---|---|---|
rewrite |
Updated the request URI, preserving the query string. | URI Rewrite |
An example:
{
"routes": [
{
"match": {
"uri": [
"/v1/*",
"/v2/*"
]
},
"action": {
"rewrite": "/app/$uri",
"pass": "applications/app"
}
},
{
"match": {
"uri": "~\\.jpe?g$"
},
"action": {
"share": [
"/var/www/static$uri",
"/var/www/static/assets$uri"
],
"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"
}
}
]
}
Variables, Scripting§
Some options in Unit configuration allow the use of variables and scripting expressions whose values are calculated at runtime.
Variables§
There’s a number of built-in variables available:
Variable | Description |
---|---|
arg_* , cookie_* , header_* |
Variables that store
request arguments, cookies, and header fields,
such as arg_queryTimeout ,
cookie_sessionId ,
or header_Accept_Encoding .
The names of the header_* variables are case insensitive. |
body_bytes_sent |
Number of bytes sent in the response body. |
dollar |
Literal dollar sign ($ ),
used for escaping. |
header_referer |
Contents of the Referer request
header field. |
header_user_agent |
Contents of the User-Agent request
header field. |
host |
Host
header field,
converted to lower case and normalized
by removing the port number
and the trailing period (if any). |
method |
Method from the request line. |
remote_addr |
Remote IP address of the request. |
request_line |
Entire request line. |
request_time |
Request processing time in milliseconds,
formatted as follows:
1.234 . |
request_uri |
Request target path including the query, normalized by resolving relative path references (“.” and “..”) and collapsing adjacent slashes. |
status |
HTTP status code of the response. |
time_local |
Local time,
formatted as follows:
31/Dec/1986:19:40:00 +0300 . |
uri |
Request 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. |
These variables can be used with:
pass
in listeners and actions to choose between routes, applications, app targets, or upstreams.rewrite
in actions to enable URI rewriting.share
andchroot
in actions to control static content serving.location
inreturn
actions to enable HTTP redirects.format
in the access log to customize Unit’s log output.
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 such characters:
{
"listeners": {
"*:80": {
"pass": "routes/${method}_route"
}
},
"routes": {
"GET_route": [
{
"action": {
"return": 201
}
}
],
"PUT_route": [
{
"action": {
"return": 202
}
}
],
"POST_route": [
{
"action": {
"return": 203
}
}
]
}
}
To reference an arg_*
,
cookie_*
,
or header_*
variable,
add the name you need to the prefix.
A query string of Type=car&Color=red
yields two variables,
$arg_Type
and $arg_Color
;
Unit additionally normalizes capitalization and hyphenation
in header field names,
so the Accept-Encoding
header field
can also be referred to as $header_Accept_Encoding
,
$header_accept-encoding
,
or $header_accept_encoding
.
Note
With multiple argument instances
(think Color=Red&Color=Blue
),
the rightmost one is used (Blue
).
At runtime, variables expand into dynamically computed values (at your risk!). The previous example targets an entire set of routes, picking individual ones by HTTP verbs from the incoming requests:
$ 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
If you reference a non-existing variable, it is considered empty.
Examples
This configuration selects the static file location based on the requested hostname; if nothing’s found, it attempts to retrieve the requested file from a common storage:
{
"listeners": {
"*:80": {
"pass": "routes"
}
},
"routes": [
{
"action": {
"share": [
"/www/$host$uri",
"/www/storage$uri"
]
}
}
]
}
Another use case 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, requests are routed between applications by their target URIs:
$ curl http://localhost/blog # Targets the 'blog' app
$ curl http://localhost/sandbox # Targets the 'sandbox' app
A different approach puts the Host
header field
received from the client
to the same use:
{
"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 use multiple variables in a string, repeating and placing them arbitrarily. This configuration picks an app target (supported for PHP and Python apps) based on the requested hostname and URI:
{
"listeners": {
"*:80": {
"pass": "applications/app_$host$uri"
}
}
}
At runtime,
a request for example.com/myapp
is passed to applications/app_example.com/myapp
.
To select a share directory
based on an app_session
cookie:
{
"action": {
"share": "/data/www/$cookie_app_session"
}
}
Here, if $uri
in share
resolves to a directory,
the choice of an index file to be served
is dictated by index
:
{
"action": {
"share": "/www/data$uri",
"index": "index.htm"
}
}
Here, a redirect uses the $request_uri
variable value
to relay the request,
including the query part,
to the same website over HTTPS:
{
"action": {
"return": 301,
"location": "https://$host$request_uri"
}
}
Scripting§
The same options that accept variables also allow template literals based on the njs scripting language. To evaluate templates (including IIFEs) and substitute them at runtime, Unit uses the njs library that ships with the official packages or can be built from source. Some request properties are available as njs variables:
Name | Description |
---|---|
args.* |
Query string arguments;
Color=Blue is args.Color ,
and so on. |
cookies.* |
Request cookies;
an authID cookie is cookies.authID ,
and so on. |
headers.* |
Request header fields;
Accept is headers.Accept ,
Content-Encoding is headers['Content-Encoding']
(hyphen requires an array property accessor),
and so on. |
host |
Host
header field,
converted to lower case and normalized
by removing the port number and the trailing period (if any). |
remoteAddr |
Remote IP address of the request. |
uri |
Request target, percent decoded and normalized by removing the query string and resolving relative references (“.” and “..”, “//”). |
Template lterals are wrapped in backticks.
To use a literal backtick in a string,
escape it: \\`
(escaping backslashes
is a
JSON requirement).
The njs-interpreted parts
should be enclosed in curly brackets:
${...}
.
This example builds a share
path
with two built-in variables
in a backtick-delimited njs template:
{
"listeners": {
"*:8080": {
"pass": "routes"
}
},
"routes": [
{
"action": {
"share": "`/www/html${host + uri}`"
}
}
]
}
Here, a request for example.com/path
will be served from /www/html/example.com/path/
.
Next, you can upload and use custom JavaScript modules
with your configuration.
Consider this http.js
script
that distinguishes requests
by their Authorization
header field values:
var http = {}
http.route = function(headers) {
var authorization = headers['Authorization'];
if (authorization) {
var user = atob(authorization.split(' ')[1]);
if (String(user) == 'user:password') {
return 'accept';
}
return 'forbidden';
}
return 'unauthorized';
}
export default http
To upload it to Unit’s JavaScript module storage
as http
:
# curl -X PUT --data-binary @http.js --unix-socket /path/to/control.unit.sock \
http://localhost/js_modules/http
Unit doesn’t enable the uploaded modules by default,
so add the module’s name to settings/js_module
:
# curl -X PUT -d '"http"' /path/to/control.unit.sock \
http://localhost/config/settings/js_module
Note
Mind that the js_module
option
can be a string or an array,
so choose the appropriate HTTP method.
Now, the http.route()
function can be used
with Unit-supplied header field values:
{
"routes": {
"entry": [
{
"action": {
"pass": "routes/`${http.route(headers)}`"
}
}
],
"unauthorized": [
{
"action": {
"return": 401
}
}
],
"forbidden": [
{
"action": {
"return": 403
}
}
],
"accept": [
{
"action": {
"return": 204
}
}
]
}
}
Examples
This example adds simple routing logic
that extracts the agent name
from the User-Agent
header field
to reject requests
issued by curl:
"routes": {
"parse": [
{
"action": {
"pass": "`routes/${ headers['User-Agent'].split('/')[0] == 'curl' ? 'reject' : 'default' }`"
}
}
],
"reject": [
{
"action": {
"return": 400
}
}
],
"default": [
{
"action": {
"return": 204
}
}
]
}
This uses a series of transformations
to parse a
JSON Web Token
from the Authorization
header field
to extract the subject claim:
{
"path": "/var/log/unit/access_kv.log",
"format": "`timestamp=${new Date().toISOString()} ip=${remoteAddr} uri=${uri} sub=${ (() => { var authz = headers['Authorization']; if (authz === undefined) { return '-'; } else { var parts = authz.slice(7).split('.').slice(0, 2).map(v => Buffer.from(v, 'base64url').toString()).map(JSON.parse); return parts[1].sub; } })() }\n`"
}
For clarity,
here’s the unwrapped njs code
that complements the sub=
log entry section:
(() => {
var authz = headers['Authorization'];
if (authz === undefined) {
return '-';
} else {
var parts = authz.slice(7).split('.').slice(0, 2).map(v => Buffer.from(v, 'base64url').toString()).map(JSON.parse);
return parts[1].sub;
}
})()
For further reference, see the njs documentation.
URI Rewrite§
All route step
actions
support the rewrite
option
that updates the URI of the incoming request
before the action is applied.
It does not affect the
query
but changes the
uri
and
$request_uri
variables.
This match
-less action
prefixes the request URI with /v1
and returns it to routing:
{
"action": {
"rewrite": "/v1$uri",
"pass": "routes"
}
}
Warning
Avoid infinite loops
when you pass
requests
back to routes
.
This action normalizes the request URI and passes it to an application:
{
"match": {
"uri": [
"/fancyAppA",
"/fancyAppB"
]
},
"action": {
"rewrite": "/commonBackend",
"pass": "applications/backend"
}
}
Instant Responses, Redirects§
You can use route step actions to instantly handle certain conditions with arbitrary HTTP status codes:
{
"match": {
"uri": "/admin_console/*"
},
"action": {
"return": 403
}
}
The return
action provides the following options:
return (required) |
Integer (000–999); defines the HTTP response status code to be returned. |
location |
String URI;
used if the return value implies redirection. |
Use the codes according to their intended semantics; if you use custom codes, make sure that 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"
}
}
Besides enriching the response semantics,
return
simplifies allow-deny lists:
instead of guarding each action with a filter,
add
conditions
to deny unwanted requests as early as possible,
for example:
"routes": [
{
"match": {
"scheme": "http"
},
"action": {
"return": 403
}
},
{
"match": {
"source": [
"!192.168.1.1",
"!10.1.1.0/16",
"192.168.1.0/24",
"2001:0db8::/32"
]
},
"action": {
"return": 403
}
}
]
Static Files§
Unit is capable of acting as a standalone web server,
efficiently serving static files
from the local file system;
to use the feature,
list the file paths
in the share
option
of a route step
action.
A share
-based action provides the following options:
share (required) |
String or an array of strings;
lists file paths that are tried
until a file is found.
When no file is found,
The value is variable-interpolated. |
index |
Filename;
tried if The default is |
fallback |
Action-like object;
used if the request
can’t be served by share or index . |
types |
Array of MIME type patterns; used to filter the shared files. |
chroot |
Directory pathname that restricts the shareable paths. The value is variable-interpolated. |
follow_symlinks , traverse_mounts |
Booleans;
turn on and off symbolic link and mount point
resolution
respectively;
if The default for both options is |
Note
To serve the files,
Unit’s router process must be able to access them;
thus, the account this process runs as
must have proper permissions
assigned.
When Unit is installed from the
official packages,
the process runs as unit:unit
;
for details of other installation methods,
see Installation.
Consider the following configuration:
{
"listeners": {
"*:80": {
"pass": "routes"
}
},
"routes": [
{
"action": {
"share": "/www/static/$uri"
}
}
]
}
It uses
variable interpolation:
Unit replaces the $uri
reference
with its current value
and tries the resulting path.
If this doesn’t yield a servable file,
a 404 “Not Found” response is returned.
Warning
Before version 1.26.0,
Unit used share
as the document root.
This was changed for flexibility,
so now share
must resolve to specific files.
A common solution is
to append $uri
to your document root.
Pre-1.26, the snippet above would’ve looked like this:
"action": {
"share": "/www/static/"
}
Mind that URI paths always start with a slash,
so there’s no need to separate the directory
from $uri
;
even if you do, Unit compacts adjacent slashes
during path resolution,
so there won’t be an issue.
If share
is an array,
its items are searched in order of appearance
until a servable file is found:
"share": [
"/www/$host$uri",
"/www/error_pages/not_found.html"
]
This snippet tries a $host
-based directory first;
if a suitable file isn’t found there,
the not_found.html
file is tried.
If neither is accessible,
a 404 “Not Found” response is returned.
Finally, if a file path points to a directory,
Unit attempts to serve an index
-indicated file from it.
Suppose we have the following directory structure
and share configuration:
/www/static/
├── ...
└──default.html
"action": {
"share": "/www/static$uri",
"index": "default.html"
}
The following request returns default.html
even though the file isn’t named explicitly:
$ curl http://localhost/ -v
...
< HTTP/1.1 200 OK
< Last-Modified: Fri, 20 Sep 2021 04:14:43 GMT
< ETag: "5d66459d-d"
< Content-Type: text/html
< Server: Unit/1.30.0
...
Note
Unit’s ETag response header fields
use the MTIME-FILESIZE
format,
where MTIME
stands for file modification timestamp
and FILESIZE
stands for file size in bytes,
both in hexadecimal.
MIME Filtering§
To filter the files a share
serves
by their
MIME types,
define a types
array of string patterns.
They work like
route patterns
but are compared to the MIME type of each file;
the request is served only if it’s a
match:
{
"share": "/www/data/static$uri",
"types": [
"!text/javascript",
"!text/css",
"text/*",
"~video/3gpp2?"
]
}
This sample configuration blocks JS and CSS files with
negation
but allows all other text-based MIME types with a
wildcard pattern.
Additionally, the .3gpp
and .3gpp2
file types
are allowed by a
regex pattern.
If the MIME type of a requested file isn’t recognized,
it’s considered empty
(""
).
Thus, the "!"
pattern
(“deny empty strings”)
can be used to restrict all file types
unknown
to Unit:
{
"share": "/www/data/known-types-only$uri",
"types": [
"!"
]
}
If a share path specifies only the directory name, Unit doesn’t apply MIME filtering.
Path Restrictions§
Note
To have these options, Unit must be built and run on a system with Linux kernel version 5.6+.
The chroot
option confines the path resolution
within a share to a certain directory.
First, it affects symbolic links:
any attempts to go up the directory tree
with relative symlinks like ../../var/log
stop at the chroot
directory,
and absolute symlinks are treated as relative
to this directory to avoid breaking out:
{
"action": {
"share": "/www/data$uri",
"chroot": "/www/data/"
}
}
Here, a request for /log
initially resolves to /www/data/log
;
however, if that’s an absolute symlink to /var/log/app.log
,
the resulting path is /www/data/var/log/app.log
.
Another effect is that any requests
for paths that resolve outside the chroot
directory
are forbidden:
{
"action": {
"share": "/www$uri",
"chroot": "/www/data/"
}
}
Here, a request for /index.xml
elicits a 403 “Forbidden” response
because it resolves to /www/index.xml
,
which is outside chroot
.
{
"action": {
"share": "/www/$host/static$uri",
"follow_symlinks": false,
"traverse_mounts": false
}
}
Here, any symlink or mount point in the entire share
path
results in a 403 “Forbidden” response.
With chroot
set,
follow_symlinks
and traverse_mounts
only affect portions of the path after chroot
:
{
"action": {
"share": "/www/$host/static$uri",
"chroot": "/www/$host/",
"follow_symlinks": false,
"traverse_mounts": false
}
}
Here, www/
and interpolated $host
can be symlinks or mount points,
but any symlinks and mount points beyond them,
including the static/
portion,
won’t be resolved.
Details
Suppose you want to serve files from a share
that itself includes a symlink
(let’s assume $host
always resolves to localhost
and make it a symlink in our example)
but disable any symlinks inside the share.
Initial configuration:
{
"action": {
"share": "/www/$host/static$uri",
"chroot": "/www/$host/"
}
}
Create a symlink to /www/localhost/static/index.html
:
$ mkdir -p /www/localhost/static/ && cd /www/localhost/static/
$ cat > index.html << EOF
> index.html
> EOF
$ ln -s index.html /www/localhost/static/symlink
If symlink resolution is enabled
(with or without chroot
),
a request that targets the symlink works:
$ curl http://localhost/index.html
index.html
$ curl http://localhost/symlink
index.html
Now set follow_symlinks
to false
:
{
"action": {
"share": "/www/$host/static$uri",
"chroot": "/www/$host/",
"follow_symlinks": false
}
}
The symlink request is forbidden, which is presumably the desired effect:
$ curl http://localhost/index.html
index.html
$ curl http://localhost/symlink
<!DOCTYPE html><title>Error 403</title><p>Error 403.
Lastly, what difference does chroot
make?
To see, remove it:
{
"action": {
"share": "/www/$host/static$uri",
"follow_symlinks": false
}
}
Now, "follow_symlinks": false
affects the entire share,
and localhost
is a symlink,
so it’s forbidden:
$ curl http://localhost/index.html
<!DOCTYPE html><title>Error 403</title><p>Error 403.
Fallback Action§
Finally, within an action
,
you can supply a fallback
option
beside a share
.
It specifies the
action
to be taken
if the requested file can’t be served
from the share
path:
{
"share": "/www/data/static$uri",
"fallback": {
"pass": "applications/php"
}
}
Serving a file can be impossible for different reasons, such as:
- The request’s HTTP method isn’t
GET
orHEAD
. - The file’s MIME type doesn’t match the
types
array. - The file isn’t found at the
share
path. - The router process has insufficient permissions to access the file or an underlying directory.
In the previous example,
an attempt to serve the requested file
from the /www/data/static/
directory
is made first.
Only if the file can’t be served,
the request is passed to the php
application.
If the fallback
itself is a share
,
it can also contain a nested fallback
:
{
"share": "/www/data/static$uri",
"fallback": {
"share": "/www/cache$uri",
"chroot": "/www/",
"fallback": {
"proxy": "http://127.0.0.1:9000"
}
}
}
The first share
tries to serve the request
from /www/data/static/
;
on failure, the second share
tries the /www/cache/
path
with chroot
enabled.
If both attempts fail,
the request is proxied elsewhere.
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$uri",
"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$uri",
"fallback": {
"proxy": "http://127.0.0.1:9000"
}
}
},
{
"action": {
"pass": "applications/php-app"
}
}
],
"applications": {
"php-app": {
"type": "php",
"root": "/www/php-app/scripts/"
}
}
}
If image files should be served locally
and other proxied,
use the types
array
in the first route step:
{
"match": {
"uri": [
"*.css",
"*.ico",
"*.jpg",
"*.js",
"*.png",
"*.xml"
]
},
"action": {
"share": "/www/php-app/assets/files$uri",
"types": [
"image/*"
],
"fallback": {
"proxy": "http://127.0.0.1:9000"
}
}
}
Another way to combine
share
, types
, and fallback
is exemplified by the following compact pattern:
{
"share": "/www/php-app/assets/files$uri",
"types": [
"!application/x-httpd-php"
],
"fallback": {
"pass": "applications/php-app"
}
}
It forwards explicit requests for PHP files
to the app
while serving all other types of files
from the share;
note that a match
object
isn’t needed here to achieve this effect.
Proxying§
Unit’s routes support HTTP proxying
to socket addresses
using the proxy
option
of a route step
action:
{
"routes": [
{
"match": {
"uri": "/ipv4/*"
},
"action": {
"proxy": "http://127.0.0.1:8080"
}
},
{
"match": {
"uri": "/ipv6/*"
},
"action": {
"proxy": "http://[::1]:8080"
}
},
{
"match": {
"uri": "/unix/*"
},
"action": {
"proxy": "http://unix:/path/to/unix.sock"
}
}
]
}
As the example 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-specific 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:
Option | Description |
---|---|
type (required) |
Application type:
Except with For example, if you have only one PHP module,
7.1.9,
it matches |
environment |
String-valued object; environment variables to be passed to the app. |
group |
String; group name that runs the app process. The default is the |
isolation |
Object; manages the isolation of an application process. For details, see here. |
limits |
Object; 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,
and object options The default is 1. |
stderr , stdout |
Strings;
filenames where Unit redirects
the application’s output
to respective streams
in The default is |
user |
String; username that runs the app process. The default is the username configured at build time or at startup. |
working_directory |
String; the app’s working directory. The default is the working directory of Unit’s main process. |
Also, you need to set type
-specific options
to run the app.
This
Python app
sets 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 has three per-app options
that control how the app’s processes behave:
isolation
, limits
, and processes
.
Also, you can GET
the /control/applications/
section of the API
to restart an app:
# curl -X GET --unix-socket /path/to/control.unit.sock \
http://localhost/control/applications/app_name/restart
Unit handles the rollover gracefully,
allowing the old processes
to deal with existing requests
and starting a new set of processes
(as defined by the processes
option)
to accept new requests.
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 mnt net pid ... user uts
The isolation
application option
has the following members:
Option | Description | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
automount |
Object;
controls mount behavior
if {
"isolation": {
"automount": {
"language_deps": false,
"procfs": false,
"tmpfs": false
}
}
}
|
||||||||||||
cgroup |
Object; defines the app’s cgroup.
|
||||||||||||
gidmap |
Same as uidmap ,
but configures group IDs instead of user IDs. |
||||||||||||
namespaces |
Object; configures namespace isolation scheme for the application. Available options (system-dependent; check your OS manual for guidance):
All options listed above are Boolean;
to isolate the app,
set the corresponding namespace option to |
||||||||||||
rootfs |
String; pathname of the directory to be used as the new file system root for the app. | ||||||||||||
uidmap |
Array of user ID mapping objects; each array item must define the following:
|
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
},
"cgroup": {
"path": "/unit/appcgroup"
},
"uidmap": [
{
"host": 1000,
"container": 0,
"size": 1000
}
],
"gidmap": [
{
"host": 1000,
"container": 0,
"size": 1000
}
]
}
Using "cgroup"
The path
value in cgroup
can be absolute
(starting with /
) or relative;
if the path doesn’t exist,
Unit creates it.
Relative paths are implicitly placed
inside the cgroup of Unit’s main process;
this setting effectively puts the app
to the /<main Unit process cgroup>/production/app
cgroup:
{
"isolation": {
"cgroup": {
"path": "production/app"
}
}
}
An absolute pathname places the application
under a separate cgroup subtree;
this configuration puts the app under /staging/app
:
{
"isolation": {
"cgroup": {
"path": "/staging/app"
}
}
}
Note
To avoid confusion,
mind that the namespaces/cgroups
option
controls the application’s cgroup namespace;
instead, the cgroup/path
option
specifies the cgroup where Unit puts the application.
Changing Root Directory§
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/"
}
}
Warning
When using rootfs
with credential
set to true
:
"isolation": {
"rootfs": "/var/app/sandbox/",
"namespaces": {
"credential": true
}
}
Ensure that the user the app runs as
can access the rootfs
directory.
Unit mounts language-specific files and directories to the new root so the app stays operational:
Language | Language-Specific Mounts |
---|---|
Java |
|
Python | Python’s sys.path
directories |
Ruby |
|
Using "uidmap", "gidmap"
The uidmap
and gidmap
options
are available only
if the underlying OS supports
user namespaces.
If uidmap
is omitted but credential
isolation is enabled,
the effective UID (EUID) of the application process
in the host namespace
is mapped to the same UID
in the container namespace;
the same applies to gidmap
and GID, respectively.
This means that the configuration below:
{
"user": "some_user",
"isolation": {
"namespaces": {
"credential": true
}
}
}
Is equivalent to the following
(assuming some_user
’s EUID and EGID are both equal to 1000):
{
"user": "some_user",
"isolation": {
"namespaces": {
"credential": true
},
"uidmap": [
{
"host": "1000",
"container": "1000",
"size": 1
}
],
"gidmap": [
{
"host": "1000",
"container": "1000",
"size": 1
}
]
}
}
Request Limits§
The limits
object
controls request handling by the app process
and has two integer options:
Option | Description |
---|---|
requests |
Integer; maximum number of requests an app process can serve. When the limit is reached, the process restarts; this mitigates possible memory leaks or other cumulative issues. |
timeout |
Integer; request timeout in seconds. If an app process exceeds it while handling a request, Unit cancels the request and returns a 503 “Service Unavailable” response to the client. Note Now, Unit doesn’t detect freezes, so the hanging process stays on the app’s process pool. |
Example:
{
"type": "python",
"working_directory": "/www/python-apps",
"module": "blog.wsgi",
"limits": {
"timeout": 10,
"requests": 1000
}
}
Application Processes§
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 a dynamic prefork model for your app,
supply a processes
object with the following options:
Option | Description |
---|---|
idle_timeout |
Number of seconds
Unit waits for
before terminating an idle process
that exceeds spare . |
max |
Maximum number of application processes that Unit maintains (busy and idle). The default is 1. |
spare |
Minimum number of idle processes
that Unit tries to maintain for an app.
When the app is started,
spare idles are launched;
Unit passes new requests to existing idles,
forking new idles
to keep the spare level
if max allows.
When busy processes complete their work
and turn idle again,
Unit terminates extra idles
after idle_timeout . |
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
}
Note
For details of manual application process restart, see here.
Go§
To run a Go app on Unit, modify its source to make it Unit-aware and rebuild the app.
Updating Go Apps to Run on Unit
Unit uses cgo to invoke C code from Go, so check the following prerequisites:
The
CGO_ENABLED
variable is set to1
:$ go env CGO_ENABLED 0 $ go env -w CGO_ENABLED=1
If you installed Unit from the official packages, install the development package:
# apt install unit-dev
# yum install unit-devel
If you installed Unit from source, install the include files and libraries:
# make libunit-install
In the import
section,
list the unit.nginx.org/go
package:
import (
...
"unit.nginx.org/go"
...
)
Replace the http.ListenAndServe
call
with unit.ListenAndServe
:
func main() {
...
http.HandleFunc("/", handler)
...
// http.ListenAndServe(":8080", nil)
unit.ListenAndServe(":8080", nil)
...
}
If you haven’t done so yet, initialize the Go module for your app:
$ go mod init example.com/app
go: creating new go.mod: module example.com/app
Install the newly added dependency and build your application:
$ go get unit.nginx.org/go@1.30.0
go: downloading unit.nginx.org
$ go build -o app app.go
If you update Unit to a newer version, repeat the two commands above to rebuild your app.
The resulting executable works as follows:
- When you run it standalone,
the
unit.ListenAndServe
call falls back tohttp
functionality. - When Unit runs it,
unit.ListenAndServe
directly communicates with Unit’s router process, ignoring the address supplied as its first argument and relying on the listener’s settings instead.
Next, configure the app on Unit; besides the common options, you have:
Option | Description |
---|---|
executable (required) |
String;
pathname of the application,
absolute or relative to working_directory . |
arguments |
Array of strings;
command-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"
]
}
Java§
First, make sure to install Unit along with the Java language module.
Besides the common options, you have:
Option | Description |
---|---|
webapp (required) |
String;
pathname
of the application’s .war file
(packaged or unpackaged). |
classpath |
Array of strings;
paths to your app’s required libraries
(may point to directories
or individual .jar files). |
options |
Array of strings; defines JVM runtime options. Unit itself
exposes the |
thread_stack_size |
Integer; stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific). The default is usually system dependent and can be set with ulimit -s <SIZE_KB>. |
threads |
Integer; number of worker threads per app process. When started, each app process creates this number of threads to handle requests. The default is |
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, OpenGrok, and Spring Boot howtos 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 app to a new system where unit-http is installed globally. Also, if you update Unit later, update the Node.js module as well according to your installation method.
Next, to run your Node.js apps on Unit, you need to configure them. Besides the common options, you have:
Option | Description |
---|---|
executable (required) |
String;
pathname of the app,
absolute or relative to Supply your #!/usr/bin/env node
Note Make sure to chmod +x the file you list here so Unit can start it. |
arguments |
Array of strings;
command-line arguments
to be passed to the app.
The example below is equivalent to
/www/apps/node-app/app.js --tmp-files /tmp/node-cache . |
Example:
{
"type": "external",
"working_directory": "/www/app/node-app/",
"executable": "app.js",
"user": "www-node",
"group": "www-node",
"arguments": [
"--tmp-files",
"/tmp/node-cache"
]
}
You can run Node.js apps without altering their code, using a loader module we provide with unit-http. Apply the following app configuration, depending on your version of Node.js:
{
"type": "external",
"executable": "/usr/bin/env",
"arguments": [
"node",
"--loader",
"unit-http/loader.mjs",
"--require",
"unit-http/loader",
"app.js"
]
}
{
"type": "external",
"executable": "/usr/bin/env",
"arguments": [
"node",
"--require",
"unit-http/loader",
"app.js"
]
}
The loader overrides the http
and websocket
modules
with their Unit-aware versions
and starts the app.
You can also run your Node.js apps without the loader
by updating the application source code.
For that, use unit-http
instead of http
in your code:
var http = require('unit-http');
To use the WebSocket protocol,
your app only needs to replace the default websocket
:
var webSocketServer = require('unit-http/websocket').server;
Perl§
First, make sure to install Unit along with the Perl language module.
Besides the common options, you have:
Option | Description |
---|---|
script (required) |
String; PSGI script path. |
thread_stack_size |
Integer; stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific). The default is usually system dependent and can be set with ulimit -s <SIZE_KB>. |
threads |
Integer; number of worker threads per app process. When started, each app process creates this number of threads to handle requests. The default is |
Example:
{
"type": "perl",
"script": "/www/bugtracker/app.psgi",
"working_directory": "/www/bugtracker",
"processes": 10,
"user": "www",
"group": "www"
}
PHP§
First, make sure to install Unit along with the PHP language module.
Besides the common options, you have:
Option | Description |
---|---|
root (required) |
String; base directory of the app’s file structure. All URI paths are relative to it. |
index |
String;
filename added to URI paths
that point to directories
if no The default is |
options |
Object;
defines
the php.ini location and options. |
script |
String;
filename of a root -based PHP script
that serves all requests to the app. |
targets |
Object;
defines application sections with
custom
root , script , and index values. |
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 specify in this option. - Otherwise, the requests are served
according to their URI paths;
if they point to directories,
index
is used.
You can customize php.ini
via the options
object:
Option | Description |
---|---|
admin , user |
Objects for extra directives.
Values in
|
file |
String;
pathname of the php.ini file with
PHP configuration directives. |
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 aof 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"
},
"user": {
"display_errors": "0"
}
}
}
Targets§
You can configure up to 254 individual entry points for a single PHP app:
{
"applications": {
"php-app": {
"type": "php",
"targets": {
"front": {
"script": "front.php",
"root": "/www/apps/php-app/front/"
},
"back": {
"script": "back.php",
"root": "/www/apps/php-app/back/"
}
}
}
}
}
Each target is an object
that specifies root
and can define index
or script
just like a regular app 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/front"
},
"127.0.0.1:80": {
"pass": "routes"
}
},
"routes": [
{
"match": {
"uri": "/back"
},
"action": {
"pass": "applications/php-app/back"
}
}
]
}
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 app level.
Python§
First, make sure to install Unit along with the Python language module.
Besides the common options, you have:
Option | Description |
---|---|
module (required) |
String; app’s module name. This module is imported by Unit the usual Python way. |
callable |
String;
name of the The default is |
home |
String;
path to the app’s
virtual environment.
Absolute or relative to Note The Python version used to run the app
is determined by ImportError: No module named 'encodings'Seeing this in Unit’s
log
after you set up
|
path |
String or an array of strings;
additional Python module lookup paths.
These values are prepended to sys.path . |
prefix |
String;
SCRIPT_NAME context value for WSGI
or the root_path context value for ASGI.
Should start with a slash
(/ ). |
protocol |
String;
hints Unit that the app uses a certain interface.
Can be asgi or wsgi . |
targets |
Object;
app sections with
custom
module and callable values. |
thread_stack_size |
Integer; stack size of a worker thread (in bytes, multiple of memory page size; the minimum value is usually architecture specific). The default is usually system dependent and can be set with ulimit -s <SIZE_KB>. |
threads |
Integer; number of worker threads per app process. When started, each app process creates this number of threads to handle requests. The default is |
Example:
{
"type": "python",
"processes": 10,
"working_directory": "/www/store/cart/",
"path": "/www/store/",
"home": ".virtualenv/",
"module": "cart.run",
"callable": "app",
"prefix": "/cart",
"user": "www",
"group": "www"
}
This snippet runs the app
callable
from the /www/store/cart/run.py
module
with /www/store/cart/
as the working directory
and /www/store/.virtualenv/
as the virtual environment;
the path
value
accommodates for situations
when some modules of the app
are imported
from outside the cart/
subdirectory.
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 with 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 tries to infer your choice automatically.
If this inference fails,
use the protocol
option
to set the interface explicitly.
Note
The prefix
option
controls the SCRIPT_NAME
(WSGI)
or root_path
(ASGI)
setting in Python’s context,
allowing to route requests
regardless of the app’s factual path.
Targets§
You can configure up to 254 individual entry points for a single Python app:
{
"applications": {
"python-app": {
"type": "python",
"path": "/www/apps/python-app/",
"targets": {
"front": {
"module": "front.wsgi",
"callable": "app"
},
"back": {
"module": "back.wsgi",
"callable": "app"
}
}
}
}
}
Each target is an object
that specifies module
and can also define callable
and prefix
just like a regular app does.
Targets can be used by the pass
options
in listeners and routes
to serve requests:
{
"listeners": {
"127.0.0.1:8080": {
"pass": "applications/python-app/front"
},
"127.0.0.1:80": {
"pass": "routes"
}
},
"routes": [
{
"match": {
"uri": "/back"
},
"action": {
"pass": "applications/python-app/back"
}
}
]
}
The home
, path
, protocol
, threads
, and
thread_stack_size
settings
are shared by all targets in the app.
Warning
If you specify targets
,
there should be no module
or callable
defined at the app level.
Moreover, you can’t combine WSGI and ASGI targets
within a single app.
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 the common options, you have:
Option | Description |
---|---|
script (required) |
String;
rack script pathname,
including the .ru extension:
/www/rubyapp/script.ru . |
hooks |
String;
pathname of the .rb file
setting the event hooks
invoked during the app’s lifecycle. |
threads |
Integer; number of worker threads per app process. When started, each app process creates this number of threads to handle requests. The default is |
Example:
{
"type": "ruby",
"processes": 5,
"user": "www",
"group": "www",
"script": "/www/cms/config.ru",
"hooks": "hooks.rb"
}
The hooks
script
is evaluated when the app starts.
If set, it can define blocks of Ruby code named
on_worker_boot
,
on_worker_shutdown
,
on_thread_boot
,
or on_thread_shutdown
.
If provided,
these blocks are called
at the respective points
of the app’s lifecycle,
for example:
@mutex = Mutex.new
File.write("./hooks.#{Process.pid}", "hooks evaluated")
# Runs once at app load.
on_worker_boot do
File.write("./worker_boot.#{Process.pid}", "worker boot")
end
# Runs at worker process boot.
on_thread_boot do
@mutex.synchronize do
# Avoids a race condition that may crash the app.
File.write("./thread_boot.#{Process.pid}.#{Thread.current.object_id}",
"thread boot")
end
end
# Runs at worker thread boot.
on_thread_shutdown do
@mutex.synchronize do
# Avoids a race condition that may crash the app.
File.write("./thread_shutdown.#{Process.pid}.#{Thread.current.object_id}",
"thread shutdown")
end
end
# Runs at worker thread shutdown.
on_worker_shutdown do
File.write("./worker_shutdown.#{Process.pid}", "worker shutdown")
end
# Runs at worker process shutdown.
Use these hooks to add custom runtime logic to your app.
Note
For Ruby-based examples, see our Ruby on Rails and Redmine howtos or a basic sample.
Settings§
Unit has a global settings
configuration object
that stores instance-wide preferences.
Option | Description |
---|---|
http |
Object; fine-tunes handling of HTTP requests from the clients. |
js_module |
String or an array of strings; lists enabled NJS modules, uploaded via the control API. |
In turn, the http
option exposes the following settings:
Option | Description |
---|---|
body_read_timeout |
Maximum number of seconds to read data from the body of a client’s request. This is 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 returns a 408 “Request Timeout” response. The default is 30. |
discard_unsafe_fields |
Boolean;
controls header field name parsing.
If it’s set to The default is |
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 returns a 408 “Request Timeout” response. The default is 30. |
idle_timeout |
Maximum number of seconds between requests in a keep-alive connection. If no new requests arrive within this interval, Unit returns a 408 “Request Timeout” response and closes the connection. The default is 180. |
log_route |
Boolean; enables or disables router logging. The default is |
max_body_size |
Maximum number of bytes in the body of a client’s request. If the body size exceeds this value, Unit returns a 413 “Payload Too Large” response and closes the connection. The default is 8388608 (8 MB). |
send_timeout |
Maximum number of seconds to transmit data as a response to the client. This is the interval between consecutive transmissions, not the time for the entire response. If no data is sent to the client within this interval, Unit closes the connection. The default is 30. |
server_version |
Boolean;
if set to The default is |
static |
Object;
configures static asset handling.
Has a single object option named # 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."
}
Defaults:
|
Access Log§
To enable basic 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."
}
By default, the log is written in the Combined Log Format. Example of a CLF 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"
Custom Log Formatting§
The access_log
option
can be also set to an object
to customize both the log path
and its format:
Option | Description |
---|---|
format |
String; sets the log format. Besides arbitrary text, can contain any variables Unit supports. |
path |
String; pathname of the access log file. |
Example:
{
"access_log": {
"path": "/var/log/unit/access.log",
"format": "$remote_addr - - [$time_local] \"$request_line\" $status $body_bytes_sent \"$header_referer\" \"$header_user_agent\""
}
}
By a neat coincidence,
the above format
is the default setting.
Also, mind that the log entry
is formed after the request has been handled.
Besides built-in variables, you can use njs templates to define the log format:
{
"access_log": {
"path": "/var/log/unit/basic_access.log",
"format": "`${host + ': ' + uri}`"
}
}