Compiling Nginx from Source #

For most system administrators, installing Nginx through a package manager (apt or dnf) or using a Docker container is more than enough for day-to-day operational needs. Those prebuilt binaries are generally compiled with the most commonly used standard modules.

However, there are times when you face special scenarios that demand building Nginx manually from its source code (compile from source). Manual compilation gives you absolute control over which features are included in Nginx, lets you add third-party modules that aren’t officially supported, and allows you to optimize the binary’s performance to match your server’s CPU architecture. This article guides you through the entire upstream compilation process, external module integration, systemd service creation, and even the zero-downtime version upgrade procedure (hot upgrade).

When Do You Need to Compile Manually? #

Building Nginx from source demands a higher maintenance commitment because you have to track new security releases and recompile manually to patch security holes. Therefore, you should only choose this path when facing the following specific needs:

  • Third-Party Module Support: You need to add community-written custom modules, such as Google’s Brotli compression module (ngx_brotli), the Web Application Firewall security module (ModSecurity), or the LDAP authentication module (nginx-auth-ldap).
  • Maximum Security Hardening: You want to disable unused built-in Nginx modules (like the mail proxy module or the auto-index module) to shrink the binary size and minimize the security attack surface.
  • Specific Hardware Optimization: You want to compile the binary with special CPU optimization flags (like AVX/AVX2 instructions for modern servers) to speed up compression and SSL/TLS encryption processes.
  • Custom Library Compatibility: You want to link Nginx to a very specific external cryptographic library version (for example, statically linking Nginx to BoringSSL to get the latest encryption features before they’re officially supported by the OS’s built-in OpenSSL).

Before going further, you can check which modules are already enabled in your system’s built-in Nginx binary with:

nginx -V 2>&1 | tr ' ' '\n' | grep module

Manual Compilation Workflow #

The compilation process systematically passes through the three main stages below:

flowchart TD
    Start["Start Manual Compilation"] --> Prep["1. Install Build Tools & Dependencies <br> (GCC, Make, OpenSSL, PCRE, zlib)"]
    Prep --> Download["2. Download & Extract Nginx Source Code <br> (Plus GPG signature verification)"]
    Download --> Config{"3. Run ./configure <br> with Option Flags & Modules"}
    
    Config -->|"Configuration Setup Successful"| Build["4. Run 'make' <br> (Compile C code into a binary)"]
    Config -->|"Failed (Missing Dependency)"| Prep
    
    Build --> Install["5. Run 'sudo make install' <br> (Copy binary & config to destination directories)"]
    Install --> Setup["6. Create the 'nginx' System Account <br> & nginx.service Systemd File"]
    Setup --> Done["Done: Nginx Ready to Run"]
    
    style Config stroke:#f57c00,stroke-width:2px
    style Build stroke:#0288d1,stroke-width:2px
    style Install stroke:#388e3c,stroke-width:2px

Stage 1: Installing Operating System Dependencies #

Nginx is written in the C programming language. Therefore, you need to install the GCC compiler toolchain (GNU Compiler Collection), the make build utility, and several development libraries required by Nginx’s core features.

The essential core libraries to install include:

  1. PCRE (Perl Compatible Regular Expressions): Needed by the HTTP Core and Rewrite modules to process regular expressions in location configuration blocks and URL redirect rules.
  2. zlib: Needed by the Gzip module to compress HTTP data before sending it to client browsers.
  3. OpenSSL: Needed by the SSL/TLS module to handle secure HTTPS encrypted connections.

You can install all these dependencies on your respective distributions:

On Ubuntu / Debian: #

sudo apt update
sudo apt install -y \
    build-essential \
    libpcre3 libpcre3-dev \
    zlib1g zlib1g-dev \
    libssl-dev \
    libgd-dev \
    git wget

On CentOS Stream / Rocky / AlmaLinux: #

sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y \
    pcre pcre-devel \
    zlib zlib-devel \
    openssl openssl-devel \
    gd gd-devel \
    git wget

Stage 2: Downloading and Verifying the Source Code #

You need to download the Nginx source code archive from the official site. You’re advised to choose the latest Stable version for production server use.

# Move to a temporary directory
cd /tmp

# Download the Nginx source code archive (change the version to the latest stable release)
wget https://nginx.org/download/nginx-1.26.1.tar.gz

# Download the cryptographic signature file (.asc) for verification
wget https://nginx.org/download/nginx-1.26.1.tar.gz.asc

For security reasons, you must verify the integrity of the downloaded archive to guarantee the code is genuinely from the Nginx team and doesn’t contain hidden malicious code:

# Import the official PGP public key belonging to the Nginx development team
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62

# Verify the digital signature on the archive
gpg --verify nginx-1.26.1.tar.gz.asc nginx-1.26.1.tar.gz
# Make sure the output shows "Good signature"

After the file’s integrity is confirmed, extract the archive and enter the source code directory:

tar -xzf nginx-1.26.1.tar.gz
cd nginx-1.26.1

Stage 3: Configuring Build Options (./configure) #

The configuration step is done by running the ./configure script. This script checks your operating system’s readiness, detects dependency library locations, and defines the installation directory locations and the list of Nginx modules you want to enable.

Here’s an example production-ready configuration that enables TLS encryption, HTTP/2, real client IP reading, thread pools for disk I/O optimization, and streaming proxy:

./configure \
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --conf-path=/etc/nginx/nginx.conf \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --pid-path=/var/run/nginx.pid \
    --lock-path=/var/run/nginx.lock \
    --user=nginx \
    --group=nginx \
    --with-http_ssl_module \
    --with-http_v2_module \
    --with-http_v3_module \
    --with-http_realip_module \
    --with-http_stub_status_module \
    --with-http_gzip_static_module \
    --with-threads \
    --with-stream \
    --with-stream_ssl_module \
    --with-compat

Explanation of Key Configuration Option Parameters: #

  • --prefix=/etc/nginx: Defines the base directory where all Nginx configuration files will be placed.
  • --sbin-path=/usr/sbin/nginx: Defines the location of the executable Nginx binary file that will be produced.
  • --user=nginx and --group=nginx: Define the credentials of the non-privileged system account that worker processes will use to handle client connections.
  • --with-http_v3_module: Enables support for HTTP/3 based on the UDP transport protocol (QUIC).
  • --with-threads: Enables the Thread Pools feature that frees the main worker processes from slow disk I/O task bottlenecks.
  • --with-compat: Enables high-level binary compatibility. This flag is crucial because it lets you load third-party dynamic modules (.so files) in the future without recompiling the entire Nginx core binary.

Stage 4: Adding Third-Party Modules (Case Study: Brotli) #

Brotli is a modern data compression algorithm developed by Google, offering far denser compression ratios than traditional Gzip without overloading the CPU. We’ll use Brotli as an example of how to insert an external module into the Nginx compilation process.

First, you need to download the ngx_brotli module repository from GitHub along with all its submodules:

cd /tmp
git clone --recurse-submodules -j8 https://github.com/google/ngx_brotli.git

Option A: Compiling the Module Statically (Static Module) #

A static module is permanently merged into the main Nginx binary file. This makes deployment very practical because you only have a single binary file.

Return to the Nginx source code directory and run ./configure with the --add-module parameter:

cd /tmp/nginx-1.26.1

# Run the configuration including the external module
./configure [insert the configuration options from Stage 3 above] \
    --add-module=/tmp/ngx_brotli

Option B: Compiling the Module Dynamically (Dynamic Module) #

A dynamic module is compiled into a separate library file ending in .so. This approach is highly favored because you can enable or disable the module in the configuration file without touching the Nginx core binary.

Use the --add-dynamic-module parameter:

cd /tmp/nginx-1.26.1

./configure [insert the configuration options from Stage 3 above] \
    --add-dynamic-module=/tmp/ngx_brotli

Stage 5: Executing the Binary Build (make & make install) #

After the ./configure step completes successfully without errors, you’re ready to build the binary with the make command. You can speed up compile time with the -j parameter, tuned to the number of CPU cores your server has:

# Compile the source code using all available CPU cores
make -j$(nproc)

The compilation process produces a binary file in the objs/nginx directory. You can install it directly into your operating system’s destination directories with:

# Copy the binary and configuration folder structure into the system
sudo make install

Stage 6: Configuring a Custom Systemd Service #

After running make install, Nginx is installed on your system, but the systemd process manager doesn’t know about this new Nginx service yet, because no service file was created automatically.

You need to create a dedicated system account for security first, then write the unit service file manually.

# Create a dedicated nginx system account if it doesn't exist yet
sudo useradd --system --no-create-home --shell /sbin/nologin nginx

# Create the log directory and give ownership to the nginx user
sudo mkdir -p /var/log/nginx
sudo chown -R nginx:nginx /var/log/nginx

Next, create the systemd unit service file:

sudo nano /etc/systemd/system/nginx.service

Fill the file with the following unit configuration, which includes Security Sandboxing features at the Linux kernel level to restrict the Nginx binary’s access rights:

[Unit]
Description=nginx - high performance web server
Documentation=https://nginx.org/en/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
# Test the configuration before the service starts
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
# Run the Master process
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
# Send the HUP (1) signal for a safe reload
ExecReload=/bin/kill -s HUP $MAINPID
# Send the TERM (15) signal for a graceful stop
ExecStop=/bin/kill -s TERM $MAINPID
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Enabling and Starting the New Service: #

# Reload systemd so it detects our new service file
sudo systemctl daemon-reload

# Enable autostart at boot
sudo systemctl enable nginx

# Start the Nginx service
sudo systemctl start nginx

# Check the service status
sudo systemctl status nginx

Loading the Dynamic Brotli Module (If You Chose the Dynamic Option) #

If you compiled Brotli as a dynamic module, the .so file will be copied to /usr/lib64/nginx/modules/ or the module directory you specified. You must load that module at the very top of the main configuration file /etc/nginx/nginx.conf:

# /etc/nginx/nginx.conf
# Load dynamic modules at the start of the file before the events/http blocks

load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

events {
    worker_connections 1024;
}

http {
    # Brotli compression configuration inside the http block
    brotli on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/javascript application/json image/svg+xml;
}

Hot Upgrade Procedure (Zero-Downtime Upgrade) #

One of the remarkable advantages of Nginx’s process architecture is its ability to upgrade the binary version without cutting a single active client connection to your server. This process is known as a Hot Upgrade.

Here’s how the old Master Process spawns a new Master Process and transparently transfers socket port handling responsibilities:

sequenceDiagram
    autonumber
    participant K as Client / Internet
    participant MO as Old Master Process (Old PID)
    participant MN as New Master Process (New PID)
    
    Note over MO: Old Nginx Version Running
    K->>MO: Send active HTTP Request
    Note over MO: Replace the /usr/sbin/nginx binary with the new version
    Admin->>MO: Send USR2 Signal (kill -USR2 Old_PID)
    MO->>MN: Spawn New Master Process (Using the new binary)
    Note over MN: New Master inherits listening sockets from the Old Master
    MN->>MN: Spawn New Worker Processes (Start serving new requests)
    K->>MN: New requests arrive & are served by the new binary
    Admin->>MO: Send WINCH Signal (kill -WINCH Old_PID)
    MO->>MO: Graceful shutdown of Old Worker Processes
    Note over MO: Old workers exit after finishing active requests
    Admin->>MO: Send QUIT Signal (kill -QUIT Old_PID)
    MO->>MO: Old Master exits peacefully
    Note over MN: Only the New Nginx Version is actively serving

Hot Upgrade Execution Steps in the Terminal: #

When you want to upgrade Nginx from 1.26.1 to the newly compiled 1.28.0:

# 1. Compile the new Nginx version in a temporary directory with the same option flags
# DO NOT run 'make install' so you don't crudely overwrite the active binary.

# 2. Back up your currently running old Nginx binary
sudo cp /usr/sbin/nginx /usr/sbin/nginx.old

# 3. Copy the newly compiled binary to replace the old one
sudo cp objs/nginx /usr/sbin/nginx

# 4. Send the USR2 signal to the old master process
# This signal orders the old master to rename its PID file to nginx.pid.oldbin
# and run a new master process using the new binary installed at /usr/sbin/nginx
sudo kill -USR2 $(cat /var/run/nginx.pid)

# Check the process status; now there are two Nginx master processes running side by side:
# One old master process and one new master process, each with their own workers.
ps aux | grep nginx

# 5. Send the WINCH signal to the old master process
# This signal orders the old master to gracefully shut down its old worker processes
sudo kill -WINCH $(cat /var/run/nginx.pid.oldbin)

# 6. Monitor server traffic. If everything runs smoothly with no client complaints,
# send the QUIT signal to the old master process to shut it down completely
sudo kill -QUIT $(cat /var/run/nginx.pid.oldbin)

If problems occur after step 4 or 5, you can roll back instantly without downtime:

# If the new binary has an error, reactivate the old workers
sudo kill -HUP $(cat /var/run/nginx.pid.oldbin)

# Send the QUIT signal to the new master to shut it down
sudo kill -QUIT $(cat /var/run/nginx.pid)

# Restore our backup binary
sudo mv /usr/sbin/nginx.old /usr/sbin/nginx

Summary #

  • Compiling from source gives absolute flexibility to insert custom external modules (like ngx_brotli or a WAF) that aren’t available in standard distro packages.
  • Make sure the core dependencies (pcre, zlib, openssl) are installed before running the configuration process.
  • Use the --with-compat flag so your Nginx binary supports loading dynamic modules (.so) without recompiling the core system in the future.
  • Create the systemd service file manually after running make install, complete with sandbox security parameters like ProtectSystem=strict.
  • Do hot upgrades using the USR2, WINCH, and QUIT signals to update the Nginx binary directly without disrupting end users (zero-downtime).

← Previous: Docker   Next: Config File Structure →

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