NGINX Unit

App Samples§

Note

These steps assume Unit was already installed with the language module for each app.

Go§

Let’s configure the following basic app, saved as /www/app.go:

package main

import (
    "io"
    "net/http"
    "unit.nginx.org/go"
)

func main() {
    http.HandleFunc("/",func (w http.ResponseWriter, r *http.Request) {
        io.WriteString(w, "Hello, Go on Unit!")
    })
    unit.ListenAndServe(":8080", nil)
}

First, create a Go module, go get Unit’s package, and build your application:

$ go mod init example.com/app
$ go get unit.nginx.org/go@1.32.1
$ go build -o /www/app /www/app.go

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/go"
      }
  },
  "applications": {
      "go": {
          "type": "external",
          "working_directory": "/www/",
          "executable": "/www/app"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, Go on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

package main

import (
    "crypto/sha256";
    "fmt";
    "io";
    "io/ioutil";
    "encoding/json";
    "net/http";
    "strings";
    "unit.nginx.org/go"
)

func formatRequest(r *http.Request) string {

    h := make(map[string]string)
    m := make(map[string]string)
    t := make(map[string]interface{})

    m["message"] = "Unit reporting"
    m["agent"] = "NGINX Unit 1.32.1"

    body, _ := ioutil.ReadAll(r.Body)
    m["body"] = fmt.Sprintf("%s", body)

    m["sha256"] = fmt.Sprintf("%x", sha256.Sum256([]byte(m["body"])))

    data, _ := json.Marshal(m)
    for name, _ := range r.Header {
        h[strings.ToUpper(name)] = r.Header.Get(name)
    }
    _ = json.Unmarshal(data, &t)
    t["headers"] = h

    js, _ := json.MarshalIndent(t, "", "    ")

    return fmt.Sprintf("%s", js)
}

func main() {
    http.HandleFunc("/",func (w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        io.WriteString(w, formatRequest(r))
    })
    unit.ListenAndServe(":8080", nil)
}

Java§

Let’s configure the following basic app, saved as /www/index.jsp:

<%@ page language="java" contentType="text/plain" %>
<%= "Hello, JSP on Unit!" %>

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/java"
      }
  },
  "applications": {
      "java": {
          "type": "java",
          "webapp": "/www/"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, JSP on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example (you’ll need to download and add the json-simple library to your app’s classpath option):

<%@ page language="java" contentType="application/json; charset=utf-8" %>
<%@ page import="com.github.cliftonlabs.json_simple.JsonObject" %>
<%@ page import="com.github.cliftonlabs.json_simple.Jsoner" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.math.BigInteger" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.security.MessageDigest" %>
<%@ page import="java.util.Enumeration" %>
<%
JsonObject r = new JsonObject();

r.put("message", "Unit reporting");
r.put("agent", "NGINX Unit 1.32.1");

JsonObject headers = new JsonObject();
Enumeration h = request.getHeaderNames();
while (h.hasMoreElements()) {
    String name = (String)h.nextElement();
    headers.put(name, request.getHeader(name));
}
r.put("headers", headers);

BufferedReader  br = request.getReader();
String          body = "";
String          line = br.readLine();
while (line != null) {
    body += line;
    line = br.readLine();
}
r.put("body", body);

MessageDigest   md = MessageDigest.getInstance("SHA-256");
byte[]          bytes = md.digest(body.getBytes(StandardCharsets.UTF_8));
BigInteger      number = new BigInteger(1, bytes);
StringBuilder   hex = new StringBuilder(number.toString(16));
r.put("sha256", hex.toString());

out.println(Jsoner.prettyPrint((Jsoner.serialize(r))));
%>

Node.js§

Let’s configure the following basic app, saved as /www/app.js:

#!/usr/bin/env node

require("unit-http").createServer(function (req, res) {
    res.writeHead(200, {"Content-Type": "text/plain"});
    res.end("Hello, Node.js on Unit!")
}).listen()

Make it executable and link the Node.js language package you’ve installed earlier:

$ cd /www
$ chmod +x app.js
$ npm link unit-http

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/node"
      }
  },
  "applications": {
      "node": {
          "type": "external",
          "working_directory": "/www/",
          "executable": "app.js"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, Node.js on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

#!/usr/bin/env node

const cr = require("crypto")
const bd = require("body")
require("unit-http").createServer(function (req, res) {
    bd (req, res, function (err, body) {
        res.writeHead(200, {"Content-Type": "application/json; charset=utf-8"})

        var r = {
            "agent":    "NGINX Unit 1.32.1",
            "message":  "Unit reporting"
        }

        r["headers"] = req.headers
        r["body"] = body
        r["sha256"] = cr.createHash("sha256").update(r["body"]).digest("hex")

        res.end(JSON.stringify(r, null, "    ").toString("utf8"))
    })
}).listen()

Note

You can run a version of the same app without requiring the unit-http module explicitly.

Perl§

Let’s configure the following basic app, saved as /www/app.psgi:

my $app = sub {
    return [
        "200",
        [ "Content-Type" => "text/plain" ],
        [ "Hello, Perl on Unit!" ],
    ];
};

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/perl"
      }
  },
  "applications": {
      "perl": {
          "type": "perl",
          "working_directory": "/www/",
          "script": "/www/app.psgi"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, Perl on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

use strict;

use Digest::SHA qw(sha256_hex);
use JSON;
use Plack;
use Plack::Request;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->header("Content-Type" => "application/json; charset=utf-8");

    my $r = {
        "message"   => "Unit reporting",
        "agent"     => "NGINX Unit 1.32.1",
        "headers"   => $req->headers->psgi_flatten(),
        "body"      => $req->content,
        "sha256"    => sha256_hex($req->content),
    };

    my $json = JSON->new();
    $res->body($json->utf8->pretty->encode($r));

    return $res->finalize();
};

PHP§

Let’s configure the following basic app, saved as /www/index.php:

<?php echo "Hello, PHP on Unit!"; ?>

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/php"
      }
  },
  "applications": {
      "php": {
          "type": "php",
          "root": "/www/"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, PHP on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

<?php

header("Content-Type: application/json; charset=utf-8");

$r = array (
   "message" => "Unit reporting",
   "agent"   => "NGINX Unit 1.32.1"
);

foreach ($_SERVER as $header => $value)
   if (strpos($header, "HTTP_") === 0)
      $r["headers"][$header] = $value;

$r["body"] = file_get_contents("php://input");
$r["sha256"] = hash("sha256", $r["body"]);

echo json_encode($r, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

?>

Python§

Let’s configure the following basic app, saved as /www/wsgi.py:

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return (b"Hello, Python on Unit!")

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/python"
      }
  },
  "applications": {
      "python": {
          "type": "python",
          "path": "/www/",
          "module": "wsgi"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, Python on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

import hashlib, json

def application(env, start_response):
    start_response("200 OK", [("Content-Type",
                               "application/json; charset=utf-8")])

    r = {}

    r["message"] = "Unit reporting"
    r["agent"] = "NGINX Unit 1.32.1"

    r["headers"] = {}
    for header in [_ for _ in env.keys() if _.startswith("HTTP_")]:
        r["headers"][header] = env[header]

    bytes = env["wsgi.input"].read()
    r["body"] = bytes.decode("utf-8")
    r["sha256"] = hashlib.sha256(bytes).hexdigest()

    return json.dumps(r, indent=4).encode("utf-8")

Ruby§

Let’s configure the following basic app, saved as /www/config.ru:

app = Proc.new do |env|
    ["200", {
        "Content-Type" => "text/plain",
    }, ["Hello, Ruby on Unit!"]]
end

run app

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
  "listeners": {
      "*:8080": {
          "pass": "applications/ruby"
      }
  },
  "applications": {
      "ruby": {
          "type": "ruby",
          "working_directory": "/www/",
          "script": "config.ru"
      }
  }
  }' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

    Hello, Ruby on Unit!

Try this sample out with the Dockerfile here or use a more elaborate app example:

require "digest"
require "json"

app = Proc.new do |env|
    body = env["rack.input"].read
    r = {
        "message" => "Unit reporting",
        "agent"   => "NGINX Unit 1.32.1",
        "body"    => body,
        "headers" => env.select { |key, value| key.include?("HTTP_") },
        "sha256"  => Digest::SHA256.hexdigest(body)
    }

    ["200", {
        "Content-Type" => "application/json; charset=utf-8",
    }, [JSON.pretty_generate(r)]]
end;

run app

WebAssembly (Wasm)§

Instead of dealing with bytecode, let’s build a Unit-capable Rust app and compile it into a WebAssembly (Wasm) component.

Make sure you have the Rust toolchain (cargo, rustc, etc.) installed. We recommend using rustup to get started.

This example was built with rustc version 1.76.0.

Start by adding the wasm32-wasi support as a compilation target for rustc

$ rustup target add wasm32-wasi

Next, install cargo component. This simplifies building a WebAssembly component from Rust Code, making it the recommended method.

$ cargo install cargo-component

Currently, the fastest way to get started with WebAssembly components using WASI 0.2 wasi-http API is the hello-wasi-http demo application by Dan Gohman. Clone the repository and build the component running the following command:

$ git clone https://github.com/sunfishcode/hello-wasi-http
$ cd hello-wasi-http
$ cargo component build

The output of the build command should be similar to this:

$ cargo component build
Compiling hello-wasi-http v0.0.0 (/home/unit-build/hello-wasi-http)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Creating component /home/unit-build/hello-wasi-http/target/wasm32-wasi/debug/hello_wasi_http.wasm
$

This creates a WebAssembly component you can deploy on Unit using the following Unit configuration. Make sure you point the component path to the WebAssembly component you have just created. Create a config.json file:

{
   "listeners": {
      "127.0.0.1:8080": {
         "pass": "applications/wasm"
      }
   },
   "applications": {
      "wasm": {
         "type": "wasm-wasi-component",
         "component": "/home/unit-build/hello-wasi-http/target/wasm32-wasi/debug/hello_wasi_http.wasm"
      }
   }
}

Apply the Unit configuration by using the CLI:

$ unitc /config < config.json

Or by sending it manually to Units control API:

$ cat config.json | curl -X PUT -d @- --unix-socket /path/to/control.unit.sock http://localhost/config/

Congratulations! You have created your very first WebAssembly component on Unit! Send a GET Request to your configured listener.

$ curl http://localhost:8080

Warning

Unit 1.32.0 and later support the WebAssembly component Model and WASI 0.2 APIs. We recommend to use the new implementation.

Instead of dealing with bytecode, let’s build a Unit-capable Rust app and compile it into WebAssembly.

Note

Currently, WebAssembly support is provided as a Technology Preview. This includes support for compiling Rust and C code into Unit-compatible WebAssembly, using our SDK in the form of the the libunit-wasm library. For details, see our unit-wasm repository on GitHub.

First, install the WebAssembly-specific Rust tooling:

$ rustup target add wasm32-wasi

Next, initialize a new Rust project with a library target (apps are loaded by Unit’s WebAssembly module as dynamic libraries). Then, add our unit-wasm crate to enable the libunit-wasm library:

$ cargo init --lib wasm_on_unit
$ cd wasm_on_unit/
$ cargo add unit-wasm

Append the following to Cargo.toml:

[lib]
crate-type = ["cdylib"]

Save some sample code from our unit-wasm repo as src/lib.rs:

wget -O src/lib.rs https://raw.githubusercontent.com/nginx/unit-wasm/main/examples/rust/echo-request/src/lib.rs

Build the Rust module with WebAssembly as the target:

$ cargo build --target wasm32-wasi

This yields the target/wasm32-wasi/debug/wasm_on_unit.wasm file (path may depend on other options).

Upload the app config to Unit and test it:

# curl -X PUT --data-binary '{
      "listeners": {
         "127.0.0.1:8080": {
            "pass": "applications/wasm"
         }
      },

      "applications": {
         "wasm": {
            "type": "wasm",
            "module": "/path/to/wasm_on_unit/target/wasm32-wasi/debug/wasm_on_unit.wasm",
            "request_handler": "uwr_request_handler",
            "malloc_handler": "luw_malloc_handler",
            "free_handler": "luw_free_handler",
            "module_init_handler": "uwr_module_init_handler",
            "module_end_handler": "uwr_module_end_handler"
         }
      }
}' --unix-socket /path/to/control.unit.sock http://localhost/config/

$ curl http://localhost:8080

      * Welcome to WebAssembly in Rust on Unit! [libunit-wasm (0.3.0/0x00030000)] *

      [Request Info]
      REQUEST_PATH = /
      METHOD       = GET
      VERSION      = HTTP/1.1
      QUERY        =
      REMOTE       = 127.0.0.1
      LOCAL_ADDR   = 127.0.0.1
      LOCAL_PORT   = 8080
      SERVER_NAME  = localhost

      [Request Headers]
      Host = localhost:8080
      User-Agent = curl/8.2.1
      Accept = */*

Further, you can research the Unit-based WebAssembly app internals in more depth. Clone the unit-wasm repository and build the examples in C and Rust (may require clang and lld):

$ git clone https://github.com/nginx/unit-wasm/
$ cd unit-wasm
$ make help                                               # Explore your options first
$ make WASI_SYSROOT=/path/to/wasi-sysroot/ examples       # C examples
$ make WASI_SYSROOT=/path/to/wasi-sysroot/ examples-rust  # Rust examples

Note

If the above commands fail like this:

wasm-ld: error: cannot open .../lib/wasi/libclang_rt.builtins-wasm32.a: No such file or directory
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Download and install the library to clang’s run-time dependency directory:

$ wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz \
      | tar zxf -                  # Unpacks to lib/wasi/ in the current directory
$ clang -print-runtime-dir         # Double-check the run-time directory, which is OS-dependent

      /path/to/runtime/dir/linux

# mkdir /path/to/runtime/dir/wasi  # Note the last part of the pathname
# cp lib/wasi/libclang_rt.builtins-wasm32.a /path/to/runtime/dir/wasi/