Skip to content

Nginx/Unit installation

Nginx web site

  • Nginx is a proxy that can also be used as load balancer, mail proxy, and HTTP cache.

Unit web site

  • Unit is a dynamic web and application server, designed to run applications in multiple languages and dynamically configured via API.

Installation

The installation of Nginx and Unit is done via the package manager of the operating system. For our case, we are using Docker and they have a Docker image with Unit installed.

Dockerfile

Unit Docker image

The Unit image is based on the python image, so the only thing that needs to be changed is the image name.

FROM unit:1.34.2-python3.12-slim AS ......

After changing the image, the next step is to install the Nginx proxy. The proxy will listen to the requests and forward them to the Unit server and in the Nginx configuration we will enable the HTTP2 protocol.

RUN apt-get update && apt-get install -y nginx && apt-get -y autoremove && apt-get -y autoclean && rm -rf /var/lib/apt/lists/*

Probably this line is already there, so we just need to add nginx to it.

Unit

Configuration

With Unit image in the dockerfile, the next step is to configure the server. The configuration is done via Unit API, the API is a RESTful API that can be accessed via HTTP requests. So we need to create a json file with the configuration and use curl to send the configuration to the server. The place where the configuration file is located is not important, but it is a good practice to put it in the same directory as the Dockerfile. Example: /var/app/docker/unit.json

{
    "listeners": {
        "127.0.0.1:<UNIT_PORT_NUMBER>": {
            "pass": "applications/starlette"
        }
    },

    "applications": {
        "starlette": {
            "type": "python 3.12",
            "path": "/var/app/",
            "protocol": "asgi",
            "module": "<FILE_NAME>",
            "callable": "<APP_OBJECT_NAME>",
            "processes": {
                "max": 20,
                "spare": 1,
                "idle_timeout": 60
            },
            "threads": 8
        }
    }
}

NOTES

  • the path is the path to the application directory in our case is /var/app.
  • the unit port number is the port that the Unit server will listen to, not the port that the application will listen to. The application port will be defined in the Nginx configuration.
  • the module is the python file name that contains the application, normally the file name is main or api.
  • the callable is the object name that will be called by the Unit server, normally the object name is app.


Start the Unit server

To start the Unit server and send the logs to stdout, we need to use the following command:

unitd --control unix:/var/run/control.unit.sock --user root --group root --pid /var/run/unit.pid --no-daemon &> /dev/stdout &

After starting the server, we need to send the configuration to the Unit server so we use curl.

curl -X PUT --retry-all-errors --retry 5 --data-binary @/var/app/docker/unit.json --unix-socket /var/run/control.unit.sock http://localhost/config/

Nginx

Configuration

The Nginx configuration is done via a configuration file. The place where the configuration file is located is not important, but it is a good practice to put it in the same directory as the Dockerfile. Example: /var/app/docker/nginx.conf

# Basic Nginx configuration
user www-data;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

# To run in foreground
daemon off;

# Check the number of CPU cores
worker_processes auto;

events {
    # Set the maximum number of simultaneous connections that can be opened by a worker process
    worker_connections 1024;

    # Use epoll for Linux because it's inside docker
    use epoll;
}

http {

    ##
    # Basic Settings
    ##
    sendfile on;
    tcp_nopush on;
    types_hash_max_size 2048;
    server_tokens off;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # Logging Settings
    ##
    # We don't need this log on Google Cloud
    access_log off;


    ##
    # Gzip Settings
    ##
    # Enable Gzip compression only when the Starlette was not doing it
    gzip off;


    ##
    # Server Settings
    ##
    # https://betterstack.com/community/questions/nginx-reverse-proxy-causing-504/
    upstream http_backend {
        # Here is the Unit server address and port
        server 127.0.0.1:<UNIT_PORT_NUMBER>;

        # Enable keepalive connections
        # This will allow the client to reuse the connection
        # https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
        keepalive 1;
    }

    server {
        # Load the PORT that this server will listen
        #
        listen <APPLICATION_PORT> http2;

        location / {
            proxy_pass http://http_backend;

            # To enable keepalive connections we need to set the following
            proxy_http_version 1.1;
            proxy_set_header Connection "";

            # Set the Host header to the original host
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            # Set the maximum time for the request to be processed
            # These times are between the Nginx and the Unit server
            # and if the Unit server takes more than 300s to process the request
            # the Nginx will return a 504 error so maybe we need to increase this time
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
            send_timeout 300s;
        }
    }
}

NOTES

  • the APPLICATION_PORT is the port that the application will be accessed.
  • the upstream server is the Unit server that was configured earlier.
  • the http2 directive only works on Google, so for the development environment, we need to remove it.


Start the Nginx proxy

To start the Nginx proxy, we use the following command:

nginx -t  -c "/var/app/docker/nginx.conf" && nginx -c "/var/app/docker/nginx.conf"
The first command is to test the configuration file and the second command is to start the Nginx proxy. It is important to test the configuration file before starting the server because if there is an error in the configuration file the Nginx proxy will not start.

run.sh version

For our case, this is the run.sh version:

    api-unit)
        PID_FILE="/var/run/unit.pid"
        UNIT_CONFIG="${PROJECT_ROOT}/docker/unit.json "
        NGINX_CONFIG="${PROJECT_ROOT}/docker/nginx.conf"
        NGINX_PORT="${PROJECT_ROOT}/docker/nginx_port.conf"

        # shellcheck disable=SC2317
        function ctrl_c() {
            ## This function is called when the Nginx is interrupted/terminated so we need to stop the Unit too
            echo "Shutting down..."
            if [ -f /var/run/unit.pid ]; then
                kill "$(cat ${PID_FILE})"
            fi
        }

        # Runs the ctrl_c function when the Nginx is interrupted
        trap ctrl_c INT

        # Run the Unit Webserver and send all logs to stdout
        unitd --control unix:/var/run/control.unit.sock --user root --group root --pid $PID_FILE --no-daemon &> /dev/stdout &

        # Set the configuration
        curl -X PUT --retry-all-errors --retry 5 --data-binary @$UNIT_CONFIG --unix-socket /var/run/control.unit.sock http://localhost/config/

        # Check if the configuration was set otherwise we need to stop the process
        if [ $? -ne 0 ]; then
            ctrl_c
            exit 1
        fi

        # Start the Nginx proxy
        if ! nginx -t  -c "${NGINX_CONFIG}" || ! nginx -c "${NGINX_CONFIG}"; then
            ctrl_c
            exit 1
        fi
    ;;