NGINX Unit

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 0 (caching is disabled).

tickets

Boolean, string, or an array of strings; configures TLS session tickets.

The default is false (tickets are disabled).

timeout

Integer; sets the session timeout for the TLS session cache.

When a new session is created, its lifetime derives from current time and timeout. If a cached session is requested past its lifetime, it is not reused.

The default is 300 (5 minutes).

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 to false.

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 “unix” string; it matches any UNIX domain sockets.

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 client_ip fields are traversed.

The default is false (no recursion).

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’s action 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:

UP \ N if P ≠ ∅
U \ N if P = ∅

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
response_headers Updates the header fields of the upcoming response. Response headers
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§

Some options in Unit configuration allow the use of variables whose values are calculated at runtime. 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_id Contains a string generated with random data. Can be used as a unique request identifier.
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.
response_header_* Variables that store response header fields, such as response_header_content_type. The names of these variables are case insensitive.
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:

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

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

Response headers§

All route step actions support the response_headers option that updates the header fields of Unit’s response before the action is taken:

{
    "action": {
        "share": "/www/static/$uri",
        "response_headers": {
            "Cache-Control": "max-age=60, s-maxage=120",
            "CDN-Cache-Control": "max-age=600"
        }
    }
}

This works only for the 2XX and 3XX responses; also, Date, Server, and Content-Length can’t be set.

The option sets given string values for the header fields of the response that Unit will send for the specific request:

  • If there’s no header field associated with the name (regardless of the case), the value is set.
  • If a header field with this name is already set, its value is updated.
  • If null is supplied for the value, the header field is deleted.

If the action is taken and Unit issues a response, it sends the header fields this specific action specifies. Only the last action along the entire routing path of a request affects the resulting response headers.

The values support variables and template literals, which enables arbitrary runtime logic:

"response_headers": {
    "Content-Language": "`${ uri.startsWith('/uk') ? 'en-GB' : 'en-US' }`"
}

Finally, there are the response_header_* variables that evaluate to the header field values set with the response (by the app, upstream, or Unit itself; the latter is the case with $response_header_connection, $response_header_content_length, and $response_header_transfer_encoding).

One use is to update the headers in the final response; this extends the Content-Type issued by the app:

"action": {
    "pass": "applications/converter",
        "response_headers": {
            "Content-Type": "${response_header_content_type};charset=iso-8859-1"
        }
    }
}

Alternatively, they will come in handy with custom log formatting.

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, fallback is used if set.

The value is variable-interpolated.

index

Filename; tried if share is a directory. When no file is found, fallback is used if set.

The default is index.html.

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 chroot is set, they only affect the insides of chroot.

The default for both options is true (resolve links and mounts).

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.32.1
 ...

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 no MIME types match the request, a 403 “Forbidden” response is returned. You can pair that behavior with a fallback option that will be called when a 40x response would be returned.

{
    "share": "/www/data/static$uri",
    "types": ["image/*", "font/*", "text/*"],
    "response_headers": {
        "Cache-Control": "max-age=1209600"
    },
    "fallback": {
        "share": "/www/data/static$uri",
    }
}

Here, all requests to images, fonts, and any text-based files will have a cache control header added to the response. Any other requests will still serve the files, but this time without the header. This is useful for serving common web page resources that do not change; web browsers and proxies are informed that this content should be cached.

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.

The follow_symlinks and traverse_mounts options disable resolution of symlinks and traversal of mount points when set to false (both default to true):

{
    "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 or HEAD.
  • 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: external (Go and Node.js), java, perl, php, python, ruby, or wasm (WebAssembly).

Except for external and wasm, 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.

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 user’s primary group.

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 max, spare, and idle_timeout enable dynamic management. For details, see here.

The default is 1.

stderr, stdout

Strings; filenames where Unit redirects the application’s output.

The default is /dev/null.

When running in --no-daemon mode, application output is always redirected to Unit’s log file.

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 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
        }
    }
}
cgroup

Object; defines the app’s cgroup.

Option Description
path (required) String; configures absolute or relative path of the app in the cgroups v2 hierarchy. The limits trickle down the hierarchy, so child cgroups can’t exceed parental thresholds.
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):

cgroup Creates a new cgroup namespace for the app.
credential Creates a new user namespace for the app.
mount Creates a new mount namespace for the app.
network Creates a new network namespace for the app.
pid Creates a new PID namespace for the app.
uname Creates 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).

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:

container Integer; starts the user ID mapping range in the app’s namespace.
host Integer; starts the user ID mapping range in the OS namespace.
size Integer; size of the ID range in both 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
    },

    "cgroup": {
        "path": "/unit/appcgroup"
    },

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

    "gidmap": [
        {
            "host": 1000,
            "container": 0,
            "size": 1000
        }
    ]
}
Using control groups§

A control group (cgroup) commands the use of computational resources by a group of processes in a unified hierarchy. Cgroups are defined by their paths in the cgroups file system.

The cgroup object defines the cgroup for a Unit app; its path option can set an absolute (starting with /) or a relative value. If the path doesn’t exist in the cgroups file system, 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"
        }
    }
}

A basic use case would be to set a memory limit on a cgroup. First, find the cgroup mount point:

$ mount -l | grep cgroup

    cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

Next, check the available controllers and set the memory.high limit:

# cat /sys/fs/cgroup//staging/app/cgroup.controllers

    cpuset cpu io memory pids

# echo 1G > /sys/fs/cgroup/staging/app/memory.high

For more details and possible options, refer to the admin guide.

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
  • JVM’s libc.so directory
  • Java module’s home directory
Python Python’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)
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 to 1:

    $ 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.32.1

      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 to http 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"
    ]
}

Note

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

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 -Dnginx.unit.context.path option that defaults to /; use it to customize the context 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 1.

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 working_directory.

Supply your .js pathname here 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.

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;

Note

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

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 1.

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 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 script is set.

The default is index.php.

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 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 can be updated at runtime.

  • The objects override the settings from any *.ini files
  • The admin object can only set what’s listed as PHP_INI_SYSTEM; for other modes, set user
  • Neither admin nor user can set directives listed as php.ini only except for disable_classes and disable_functions
file String; pathname of the php.ini file with PHP configuration directives.

To load multiple .ini files, use environment with PHP_INI_SCAN_DIR to scan a custom directory:

{
    "applications": {
        "hello-world": {
            "type": "php",
            "root": "/www/public/",
            "script": "index.php",
            "environment": {
                "PHP_INI_SCAN_DIR": ":/tmp/php.inis/"
            }
        }
    }
}

Mind that the colon that prefixes the value here is a path separator; it causes PHP to scan the directory preconfigured with the --with-config-file-scan-dir option, which is usually /etc/php.d/, and then the directory you set here, which is /tmp/php.inis/. To skip the preconfigured directory, drop the : prefix.

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.

Note

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

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 module-based callable that Unit runs as the app.

The default is application.

home

String; path to the app’s virtual environment. Absolute or relative to working_directory.

Note

The Python version used to run the app is determined by type; for performance, Unit doesn’t use the command-line interpreter from the virtual environment.

ImportError: No module named 'encodings'

Seeing this in Unit’s log after you set up home for your app? This usually occurs if the interpreter can’t use the virtual environment, possible reasons including:

  • Version mismatch between the type setting and the virtual environment; check the environment’s version:

    $ source /path/to/venv/bin/activate
    (venv) $ python --version
    
  • Unit’s unprivileged user (usually unit) having no access to the environment’s files; assign the necessary rights:

    # chown -R unit:unit /path/to/venv/
    
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 1.

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, for instance: /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 1.

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.

WebAssembly§

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

Besides the common options, you have:

Option Description
component (required) String; WebAssembly component pathname, including the .wasm extension, for instance: “/var/www/wasm/component.wasm”
access

Object; its only array member, filesystem, lists directories to which the application has access:

"access": {
   "filesystem": [
      "/tmp/",
      "/var/tmp/"
   ]
}

Example:

{
  "listeners": {
     "127.0.0.1:8080": {
        "pass": "applications/wasm"
     }
  },
  "applications": {
     "wasm": {
        "type": "wasm-wasi-component",
        "component": "/var/www/app/component.wasm",
        "access": {
        "filesystem": [
           "/tmp/",
           "/var/tmp/"
        ]
        }
     }
  }
}

Note

A good, first Rust-based project is available at sunfishcode/hello-wasi-http. It also includes all the important steps to get started with WebAssembly, WASI, and Rust.

Warning

The unit-wasm module is deprecated. We recommend using wasm-wasi-component instead, which supports WebAssembly Components using standard WASI 0.2 interfaces. The wasm-wasi-component module is available in Unit 1.32 and later.

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

Besides the common options, you have:

Option Description
module (required) String; WebAssembly module pathname, including the .wasm extension, for instance: applications/wasmapp/module.wasm.
request_handler (required)

String; name of the request handler function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

The runtime calls this handler, providing the address of the shared memory block used to pass data in and out the app.

malloc_handler (required)

String; name of the memory allocator function. See note above regarding language-specific handlers in the official unit-wasm package.

The runtime calls this handler at language module startup to allocate the shared memory block used to pass data in and out the app.

free_handler (required)

String; name of the memory deallocator function. See note above regarding language-specific handlers in the official unit-wasm package.

The runtime calls this handler at language module shutdown to free the shared memory block used to pass data in and out the app.

access

Object; its only array member, filesystem, lists directories the application can access:

"access": {
   "filesystem": [
      "/tmp/",
      "/var/tmp/"
   ]
}
module_init_handler,

String; name of the module initilization function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

It is invoked by the WebAssembly language module at language module startup, after the WebAssembly module was initialized.

module_end_handler

String; name of the module finalization function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

It is invoked by the WebAssembly language module at language module shutdown.

request_init_handler

String; name of the request initialization function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

It is invoked by the WebAssembly language module at the start of each request.

request_end_handler

String; name of the request finalization function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

It is invoked by the WebAssembly language module at the end of each request, when the headers and the request body were received.

response_end_handler

String; name of the response finalization function. If you use Unit with the official unit-wasm package, the value is language specific; see the SDK documentation for details. Otherwise, use the name of your custom implementation.

It is invoked by the WebAssembly language module at the end of each response, when the headers and the response body were sent.

Example:

{
    "type": "wasm",
    "module": "/www/webassembly/unitapp.wasm",
    "request_handler": "my_custom_request_handler",
    "malloc_handler": "my_custom_malloc_handler",
    "free_handler": "my_custom_free_handler",
    "access": {
        "filesystem": [
            "/tmp/",
            "/var/tmp/"
        ]
    },
    "module_init_handler": "my_custom_module_init_handler",
    "module_end_handler": "my_custom_module_end_handler",
    "request_init_handler": "my_custom_request_init_handler",
    "request_end_handler": "my_custom_request_end_handler",
    "response_end_handler": "my_custom_response_end_handler"
}

Use these handlers to add custom runtime logic to your app; for a detailed discussion of their usage and requirements, see the SDK source code and documentation.

Note

For WASM-based examples, see our Rust and C samples.

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 true, Unit only processes header names made of alphanumeric characters and hyphens (see RFC 9110); otherwise, these characters are also permitted: .!#$%&’*+^_`|~.

The default is true.

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 false (disabled).

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 false, Unit omits version information in its Server response header fields.

The default is true.

static

Object; configures static asset handling. Has a single object option named mime_types that defines specific MIME types as options. Their values can be strings or arrays of strings; each string must specify a filename extension or a specific filename that’s included in the MIME type. You can override default MIME types or add new types:

# 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: .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, .php, .png, .rpm, .rss, .rst, .svg, .ttf, .txt, .wav, .webm, .webp, .woff2, .woff, .xml, and .zip.

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}`"
    }
}

Conditional access log§

The access_log can be dynamically turned on and off by using the if option:

Option Description
if if the value is empty, 0, false, null, or undefined, the logs will not be recorded.

This feature lets users set conditions to determine whether access logs are recorded. The if option supports a string and JavaScript code. If its value is empty, 0, false, null, or undefined, the logs will not be recorded. And the ‘!’ as a prefix inverses the condition.

Example without njs:

{
   "access_log": {
      "if": "$cookie_session",
      "path": "..."
   }
}

All requests using a session cookie named session will be logged.

We can add ! to inverse the condition.

{
   "access_log": {
      "if": "!$cookie_session",
      "path": "..."
   }
}

Now, all requests without a session cookie will be logged.

Example with njs and the use of a template literal:

{
   "access_log": {
      "if": "`${uri == '/health' ? false : true}`",
      "path": "..."
   }
}