Dynamic Module #

Before Nginx version 1.9.11 was released, every time we wanted to add a new module to our Nginx web server, we were forced to recompile the entire Nginx binary from source code. This process was very time-consuming, triggered system configuration error risks, and required long downtime because we had to stop and replace the main server binary.

The introduction of Dynamic Modules revolutionized Nginx’s modular architecture. Dynamic modules are compiled separately into shared library files (shared objects with the .so extension) and dynamically loaded into RAM by the core Nginx binary at runtime using the load_module directive. In this article, we’ll discuss the deep architectural differences between static vs dynamic, manage module storage locations, dissect binary compatibility issues, and put together a guide for writing our own custom dynamic C modules.

Architecture Comparison: Static vs Dynamic Module #

To understand the difference in how static vs dynamic modules are compiled and loaded into memory, look at the following architecture comparison diagram:

flowchart TD
    subgraph Static["Static Module Architecture (Old Way)"]
        SrcCode["Nginx Source + Module Source"] --> Compiler["C Compiler (make)"]
        Compiler --> Binary["Single Large Binary (nginx)"]
        Binary --> RAMSt["Loaded Whole in RAM"]
    end

    subgraph Dynamic["Dynamic Module Architecture (New Way)"]
        SrcNginx["Nginx Core Source"] --> CompilerN["C Compiler"]
        CompilerN --> CoreBinary["Small Core Binary (nginx)"]
        SrcModule["Module Source (.c)"] --> CompilerM["C Compiler"]
        CompilerM --> SharedObject["Shared Object (.so)"]
        
        CoreBinary -.-> LoadModule["load_module in nginx.conf"]
        SharedObject -.-> LoadModule
        LoadModule --> RAMDy["Dynamically Loaded in RAM at Runtime"]
    end

    classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
    classDef stStyle fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b;
    classDef dyStyle fill:#d1fae5,stroke:#10b981,stroke-width:2px,color:#065f46;
    class Binary,RAMSt stStyle;
    class CoreBinary,SharedObject,RAMDy dyStyle;

Here’s a comparative functional analysis table between the two module types:

CharacteristicStatic Module (Old Way)Dynamic Module (New Way)
Memory LoadingCompiled dead into one main binary.Loaded from an external .so file at startup.
Binary SizeLarge, because it carries all built-in/external modules.Small and minimal (only the core engine).
Update EaseMust recompile all of Nginx to add a module.Just replace or add a new .so file on disk.
Update DowntimeHigh, because it overwrites the running main binary.Low/Zero, just reload the Nginx configuration.
Performance OverheadVery Low (direct local function calls).Low (small overhead from dynamic linking).
Version Compatibility100% Safe (locked at the binary compilation level).Very Strict (must be compiled with the same version).

How to Load a Dynamic Module in nginx.conf #

To enable a dynamic module we already have, we use the load_module directive. The placement of this directive must be at the very top of our nginx.conf file (main context), right before the events {} or http {} configuration blocks.

Example of Correct Syntax Writing #

# 1. Top Part of the nginx.conf File (Main Context)
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
load_module modules/ngx_http_headers_more_filter_module.so;

# 2. Other Main Configuration Blocks
events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    # ... other web server configuration ...
}

The .so file path given to the load_module directive can be written relative to our Nginx configuration prefix directory (usually /etc/nginx/) or written absolutely like /usr/lib/nginx/modules/ngx_http_geoip2_module.so.


Module Directory Management on Various OSes #

Each Linux distribution places Nginx shared object .so files in different standard directory locations:

  • Ubuntu / Debian (APT):
    • Active modules are stored at /usr/lib/nginx/modules/.
    • The Debian system uses a modular loading structure: loading configurations are stored in /etc/nginx/modules-available/ (e.g., 50-mod-http-brotli.conf) which are then enabled using symbolic links (symlinks) to /etc/nginx/modules-enabled/.
  • CentOS / RHEL / Rocky Linux (YUM/DNF):
    • Dynamic modules are stored at /usr/lib64/nginx/modules/ or /etc/nginx/modules/.
  • Manual Compilation from Source:
    • By default stored at /usr/local/nginx/modules/.

The modules-available & modules-enabled Structure (Debian Standard) #

On Ubuntu/Debian systems, the default nginx.conf file automatically loads all active modules with the following directive at its top lines:

include /etc/nginx/modules-enabled/*.conf;

If we compile a new module, we should neatly create its configuration file:

# 1. Create its configuration file
echo "load_module modules/ngx_http_my_module.so;" | sudo tee /etc/nginx/modules-available/60-mod-my-module.conf

# 2. Enable it with a symlink
sudo ln -s /etc/nginx/modules-available/60-mod-my-module.conf /etc/nginx/modules-enabled/

# 3. Test & reload Nginx
sudo nginx -t && sudo systemctl reload nginx

The Main Obstacle: Nginx Binary Compatibility #

The most important limitation to understand when managing dynamic modules is strict binary compatibility.

The .so module file must be compiled using:

  1. The exact same Nginx version as the Nginx version running in production. We can’t use a module compiled for Nginx 1.24.0 on Nginx 1.24.1 (even though it’s only a patch version difference).
  2. Compatible compilation configuration options (./configure). If our main Nginx is compiled with SSL support, our dynamic module must also be compiled with the same support included.

Detecting Binary Compatibility Failures #

If we load a binary-incompatible module, Nginx refuses to start and outputs the following critical error message when we run the configuration test (nginx -t):

nginx: [emerg] module "/usr/lib/nginx/modules/ngx_http_brotli_filter_module.so" is not binary compatible in /etc/nginx/nginx.conf:2

Solutions for Compatibility Errors #

To solve this problem, we must download the Nginx source code of the matching version, then recompile the .so module including the --with-compat option as discussed in the third-party module section.


[NEW] Writing a Simple Custom Dynamic C Module #

To understand how Nginx interacts with dynamic modules under the hood, let’s design a simple dynamic C module named ngx_http_developer_header_module. This module’s job is to add a custom HTTP header "X-Developer: Antigravity" to every response served by the server.

The process of creating this custom C module is divided into 3 main files:

1. C Source Code File: ngx_http_developer_header_module.c #

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

// Request handler declaration
static ngx_int_t ngx_http_developer_header_handler(ngx_http_request_t *r);
static ngx_int_t ngx_http_developer_header_init(ngx_conf_t *cf);

// HTTP module context
static ngx_http_module_t ngx_http_developer_header_module_ctx = {
    NULL,                                  /* preconfiguration */
    ngx_http_developer_header_init,        /* postconfiguration */
    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */
    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */
    NULL,                                  /* create location configuration */
    NULL                                   /* merge location configuration */
};

// Main Nginx module definition
ngx_module_t ngx_http_developer_header_module = {
    NGX_MODULE_V1,
    &ngx_http_developer_header_module_ctx, /* module context */
    NULL,                                  /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

// Module initialization function into the Nginx cycle
static ngx_int_t
ngx_http_developer_header_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    // Insert our handler into the HTTP HEADERS_FILTER phase
    h = ngx_array_push(&cmcf->phases[NGX_HTTP_HEADERS_FILTER_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_developer_header_handler;

    return NGX_OK;
}

// Response header processing logic
static ngx_int_t
ngx_http_developer_header_handler(ngx_http_request_t *r)
{
    ngx_table_elt_t  *h;

    // Allocate memory for the new header
    h = ngx_list_push(&r->headers_out.headers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    // Set the header key: X-Developer
    h->hash = 1;
    ngx_str_set(&h->key, "X-Developer");
    ngx_str_set(&h->value, "Antigravity");

    return NGX_DECLINED; // Continue the process to the next filter
}

Dissecting the Components and Anatomy of Nginx Module C Code #

Writing a module in C for Nginx requires a deep understanding of the Nginx core engine design principles:

  • The ngx_str_t Data Type: Nginx doesn’t use standard C strings (char*) terminated by the null character \0. Instead, Nginx defines the ngx_str_t data type with the structure:

    typedef struct {
        size_t      len;   // String length
        u_char     *data;  // Pointer to the character array
    } ngx_str_t;
    

    This design has two big advantages. First, Nginx doesn’t need to call the strlen() function, which consumes CPU resources, to calculate text length. Second, Nginx can do safe substring slicing just by shifting the data pointer and shrinking len, without needing to allocate new memory in RAM (zero-copy operation).

  • Memory Pool Management (r->pool): Inside Nginx C modules, we don’t use standard C memory allocation functions like malloc() or free(). Nginx implements a Memory Pool system. Every incoming HTTP request has its own memory pool (r->pool).

    When we want to allocate data (like adding a header), we call the ngx_palloc(r->pool, size) function. Nginx takes memory space from the pool that was allocated at the start. The main advantage is that we don’t need to manually free memory using free(). When the HTTP transaction finishes and the connection closes, Nginx automatically destroys the entire memory pool in bulk. This system completely eliminates the risk of memory leaks caused by developer negligence.

  • The Header Filtering Phase (HEADERS_FILTER_PHASE): In the ngx_http_developer_header_init function, our module registers itself into the NGX_HTTP_HEADERS_FILTER_PHASE phase. This is the filtering phase right before the response headers are sent to the network socket. Here, we call ngx_list_push to add a new element to the linked-list chain r->headers_out.headers that holds all outgoing HTTP headers.


2. Compiler Configuration File: config #

Nginx needs a file named config (without an extension) in our module directory to tell the compiler how to build the .so file:

ngx_addon_name=ngx_http_developer_header_module

if test -n "$ngx_module_link"; then
    ngx_module_type=HTTP
    ngx_module_name=ngx_http_developer_header_module
    ngx_module_srcs="$ngx_addon_dir/ngx_http_developer_header_module.c"

    . auto/module
else
    HTTP_MODULES="$HTTP_MODULES ngx_http_developer_header_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_developer_header_module.c"
fi
  • ngx_module_link: This variable is checked by the Nginx configure script. If we compile as a dynamic module (using --add-dynamic-module), this variable will have the value DYNAMIC and Nginx will build the .so shared object file. If the value is empty, the module will be built statically into the main Nginx binary.
  • $ngx_addon_dir: An automatic variable referring to the absolute path of the folder where our custom module is stored when we call the ./configure script.

3. Compilation Steps into a .so file #

Put both files above in a custom folder (e.g., /home/user/my_module/), then run the compilation from inside our Nginx source code directory:

cd /home/user/nginx-1.24.0
./configure --with-compat --add-dynamic-module=/home/user/my_module/
make modules

After the compilation finishes, the dynamic binary file ngx_http_developer_header_module.so will be created inside the objs/ directory. Copy this file to /etc/nginx/modules/ and call it using load_module in our nginx.conf.


Server Upgrade Automation Workflow (CI/CD Best Practices) #

Because of binary compatibility limitations, automatic Nginx package updates (like apt upgrade) can trigger server startup failures if our self-compiled dynamic modules aren’t updated simultaneously.

To automate this in advanced production environments, we can apply the following practices:

  1. Use DPKG Hooks (Ubuntu/Debian): We can create a script at /etc/apt/apt.conf.d/99compile-modules that automatically triggers a git pull and shared object module recompilation every time a new version installation of the nginx-core or nginx-common packages is detected.
  2. Central CI/CD Pipeline: Use a Docker container or Gitlab Runner to periodically check for Nginx updates. The pipeline automatically compiles .so files for every new Nginx version, tests them using nginx -t in an isolated container, then distributes them to our entire production server fleet if the test succeeds.

Summary and Best Practices #

  • Must Use load_module in the Main Context: Make sure the load_module directive line is never written inside the http {} or events {} blocks. Writing it inside those configuration blocks will trigger critical syntax failures at Nginx startup.
  • Use Distribution Packages If Possible: If the dynamic module we need is already provided by our official Linux repository packages (e.g., libnginx-mod-http-brotli), prioritize installation via the package manager (apt or yum) because binary compatibility updates will be automatically managed by the OS.
  • Reload, Not Restart: Use systemctl reload nginx (or nginx -s reload) when enabling new dynamic modules. This step guarantees the module loading process into new worker memory happens without cutting active client connections in progress.

← Previous: Third-Party Modules   Next: Node.js Application →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact