Introduction

Modules in Nginx can be either dynamic or static. A dynamic module is a shared object that is loaded by Nginx at runtime and Nginx need to be instructed in the configuration to load the module.

A static module is compiled into the Nginx binary and, hence, does not need to be specified in Nginx configuration.

With dynamic modules you can usually use the Nginx version provided by the package manager of your operating system so you only build the module and update the configuration.

With a static module you will need to build your own Nginx binary.

This note will describe how to use CMake to build Nginx modules. This hello world nginx module has been used for testing.

Determine Nginx build options

When building a dynamically loadable module one will need the Nginx source code. The Nginx source code used when building the module need to have the same version as the Nginx binary where the module should be loaded.

Check the version and configure arguments using command (output is an example from Nginx in homebrew for OS X):

$ nginx -V
nginx version: nginx/1.23.2
built by clang 14.0.0 (clang-1400.0.29.202)
built with OpenSSL 1.1.1q  5 Jul 2022 (running with OpenSSL 1.1.1s  1 Nov 2022)
TLS SNI support enabled
configure arguments: --prefix=/opt/homebrew/Cellar/nginx/1.23.2 --sbin-path=/opt/homebrew/Cellar/nginx/1.23.2/bin/nginx --with-cc-opt='-I/opt/homebrew/opt/pcre2/include -I/opt/homebrew/opt/openssl@1.1/include' --with-ld-opt='-L/opt/homebrew/opt/pcre2/lib -L/opt/homebrew/opt/openssl@1.1/lib' --conf-path=/opt/homebrew/etc/nginx/nginx.conf --pid-path=/opt/homebrew/var/run/nginx.pid --lock-path=/opt/homebrew/var/run/nginx.lock --http-client-body-temp-path=/opt/homebrew/var/run/nginx/client_body_temp --http-proxy-temp-path=/opt/homebrew/var/run/nginx/proxy_temp --http-fastcgi-temp-path=/opt/homebrew/var/run/nginx/fastcgi_temp --http-uwsgi-temp-path=/opt/homebrew/var/run/nginx/uwsgi_temp --http-scgi-temp-path=/opt/homebrew/var/run/nginx/scgi_temp --http-log-path=/opt/homebrew/var/log/nginx/access.log --error-log-path=/opt/homebrew/var/log/nginx/error.log --with-compat --with-debug --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_degradation_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-ipv6 --with-mail --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module

Make sure that the --with-compat options is there. If not, you may need to use the exact same config options when building the module.

Build a dynamic module

Using a CMake external project is convenient because then source code can be downloaded, configured and built using any command.

Create a CMakeLists.txt file in the module directory and put mandatory directives in the top of the file

cmake_minimum_required(VERSION 3.24)
project(nginx-module)

then create an external project for Nginx like this:

include(ExternalProject)

ExternalProject_Add(
    nginx
    DOWNLOAD_COMMAND URL https://nginx.org/download/nginx-1.23.2.tar.gz
    CONFIGURE_COMMAND cd ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx && ./configure --add-dynamic-module=${CMAKE_BINARY_DIR}/../ --with-compat
    BUILD_COMMAND make -C ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx modules
    INSTALL_COMMAND ""
)

As seen the configure command uses --add-dynamic-module to point out the directory of the module. When using CMake a separate subdirectory is used for doing the build in the nginx module directory, that is why we use ${CMAKE_BINARY_DIR}/...

Make sure to use the same version of Nginx as where you will load the module. If not, Nginx will fail to start and show an error when loading the module. This is an example when Nginx 1.23.2 is loading a module build usinh Nginx 1.18 source code:

nginx: [emerg] module "/opt/homebrew/Cellar/nginx/1.23.2/modules/ngx_http_hello_world_module.so" version 1018000 instead of 1023002 in /opt/homebrew/etc/nginx/nginx.conf:11

You can also add a custom command to copy the module shared object into a build output directory to the CMakeLists.txt-file:

add_custom_command(
     TARGET nginx
     COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/output
     COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx/objs/*.so ${CMAKE_SOURCE_DIR}/output/
     COMMENT "Copy nginx module to ${CMAKE_SOURCE_DIR}/output/"
 )

Then build the module

$ cmake -B build
$ cmake --build build --parallel 8

The module will be found in the output directory.

Load the dynamic module

From the configuration options (seen in nignx -V) you can see from what location Nginx is loading its modules in the option --modules-path. If this option is not specified, the modules are loaded from the modules directory under the path specified in --prefix. In the example above the modules will be loaded from /opt/homebrew/Cellar/nginx/1.23.2/modules

Copy the module to the modules directory. Then in the configuration add

load_module "modules/ngx_hello_world_module.so";

Then start Nginx or reload config

$ nginx -s reload

To test the hello world module just add a test location to the config which is using the directive defined in the module

location /test {
    hello_world;
}

Then reload the config and curl the new location:

$ nginx -s reload
$ curl localhost/test
hello world

Build a static module

Building a static module means that the module is built into the Nginx binary, so you basically build Nginx and specify a path to the module.

Add this CMakeLists.txt into your module directory to build Nginx containing the static module

ExternalProject_Add(
    nginx
    DOWNLOAD_COMMAND URL https://nginx.org/download/nginx-1.18.0.tar.gz
    CONFIGURE_COMMAND cd ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx && ./configure --add-module=${CMAKE_BINARY_DIR}/../ --with-compat --prefix=/opt/nginx
    BUILD_COMMAND make -C ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx
    INSTALL_COMMAND make -C ${CMAKE_CURRENT_BINARY_DIR}/nginx-prefix/src/nginx install
)

Build with:

$ cmake -B build
$ cmake --build build --parallel 8

The resulting build can be found in /opt/nginx

To test the hello world module just add a test location using the directive defined in the module. Note that you don’t need load_module since the module is already in the Nginx binary.

location /test {
    hello_world;
}

Then reload the config and curl

$ nginx -s reload
$ curl localhost/test
hello world

References

Nginx Hello World module

Compiling dynamic modules for nginx

Converting static module to dynamic

How dynamic modules work