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:
| Characteristic | Static Module (Old Way) | Dynamic Module (New Way) |
|---|---|---|
| Memory Loading | Compiled dead into one main binary. | Loaded from an external .so file at startup. |
| Binary Size | Large, because it carries all built-in/external modules. | Small and minimal (only the core engine). |
| Update Ease | Must recompile all of Nginx to add a module. | Just replace or add a new .so file on disk. |
| Update Downtime | High, because it overwrites the running main binary. | Low/Zero, just reload the Nginx configuration. |
| Performance Overhead | Very Low (direct local function calls). | Low (small overhead from dynamic linking). |
| Version Compatibility | 100% 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/.
- Active modules are stored at
- CentOS / RHEL / Rocky Linux (YUM/DNF):
- Dynamic modules are stored at
/usr/lib64/nginx/modules/or/etc/nginx/modules/.
- Dynamic modules are stored at
- Manual Compilation from Source:
- By default stored at
/usr/local/nginx/modules/.
- By default stored at
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:
- The exact same Nginx version as the Nginx version running in production. We can’t use a module compiled for Nginx
1.24.0on Nginx1.24.1(even though it’s only a patch version difference). - 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_tData Type: Nginx doesn’t use standard C strings (char*) terminated by the null character\0. Instead, Nginx defines thengx_str_tdata 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 thedatapointer and shrinkinglen, 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 likemalloc()orfree(). 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 usingfree(). 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 thengx_http_developer_header_initfunction, our module registers itself into theNGX_HTTP_HEADERS_FILTER_PHASEphase. This is the filtering phase right before the response headers are sent to the network socket. Here, we callngx_list_pushto add a new element to the linked-list chainr->headers_out.headersthat 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 valueDYNAMICand Nginx will build the.soshared 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./configurescript.
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:
- Use DPKG Hooks (Ubuntu/Debian):
We can create a script at
/etc/apt/apt.conf.d/99compile-modulesthat automatically triggers a git pull and shared object module recompilation every time a new version installation of thenginx-coreornginx-commonpackages is detected. - Central CI/CD Pipeline:
Use a Docker container or Gitlab Runner to periodically check for Nginx updates. The pipeline automatically compiles
.sofiles for every new Nginx version, tests them usingnginx -tin 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_moduledirective line is never written inside thehttp {}orevents {}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 (aptoryum) because binary compatibility updates will be automatically managed by the OS.- Reload, Not Restart: Use
systemctl reload nginx(ornginx -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.