Skip to main content

NGINX

NGINX CGI Module: Run CGI Scripts in NGINX

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

NGINX does not natively support the Common Gateway Interface (CGI) protocol. The NGINX CGI module fills this gap by bringing full RFC 3875 CGI/1.1 support to NGINX, allowing you to execute CGI scripts written in any language. If you are migrating from Apache to NGINX and have legacy CGI scripts, the NGINX CGI module lets you run them without rewriting your applications.

In this guide, you will learn how to install the NGINX CGI module, configure it for your server, and migrate your existing Apache CGI setup to NGINX.

Why Use CGI with NGINX?

CGI (Common Gateway Interface) is one of the oldest web technologies. It works by spawning a new process for every HTTP request, executing a script, and returning the output. While this approach is less efficient than FastCGI or application servers, CGI remains useful for:

  • Legacy applications — many enterprise and government systems still rely on CGI scripts written decades ago
  • System administration tools — low-frequency management interfaces where performance is not critical
  • Embedded systems — resource-constrained devices where a full application server is overkill
  • Prototyping — quickly testing ideas without setting up FastCGI or application servers
  • Simple automation — webhook handlers, CI/CD callbacks, and similar one-off scripts

However, CGI is not suitable for high-traffic production websites. Each request spawns a new process, which creates significant overhead. For high-performance applications, use FastCGI with PHP-FPM or an application server instead.

CGI vs. FastCGI: Understanding the Difference

Before proceeding, it is important to understand the distinction between CGI and FastCGI:

Feature CGI FastCGI
Process lifecycle New process per request Persistent process pool
Performance Slower (process spawn overhead) Faster (reuses processes)
Memory usage Lower at rest, higher under load Higher at rest, lower under load
NGINX support Requires nginx-cgi module Built-in fastcgi_pass
Best for Low-traffic, legacy, admin tools Production applications

The NGINX CGI module fills the gap for scenarios where FastCGI is not an option — typically when migrating legacy applications from Apache.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-cgi

Then load the module by adding the following line to the top of /etc/nginx/nginx.conf (before the http {} block):

load_module modules/ngx_http_cgi_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-cgi

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

For more details, see the APT module page.

Basic Configuration

The simplest NGINX CGI configuration enables CGI for a specific location:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    location /cgi-bin {
        cgi on;
    }
}

With this configuration, any executable script under /var/www/html/cgi-bin/ will be treated as a CGI script. For example, a request to /cgi-bin/hello.sh will execute /var/www/html/cgi-bin/hello.sh.

Creating Your First CGI Script

Create a simple CGI script at /var/www/html/cgi-bin/hello.sh:

#!/bin/bash
echo "Content-Type: text/plain"
echo
echo "Hello from NGINX CGI!"
echo "Server: $SERVER_SOFTWARE"
echo "Method: $REQUEST_METHOD"
echo "Query: $QUERY_STRING"

Make it executable:

chmod +x /var/www/html/cgi-bin/hello.sh

Test it:

curl http://localhost/cgi-bin/hello.sh

Expected output:

Hello from NGINX CGI!
Server: nginx/1.28.2
Method: GET
Query:

CGI Script Output Format

A CGI script must output two sections separated by an empty line:

  1. Header section — HTTP response headers, one per line
  2. Body section — the response body

The Status header controls the HTTP status code. If omitted, NGINX returns 200 OK. For example, to return a 404 response:

#!/bin/bash
echo "Status: 404"
echo "Content-Type: text/plain"
echo
echo "Page not found"

To send a redirect:

#!/bin/bash
echo "Status: 302"
echo "Location: https://example.com/new-location"
echo

Important: Shebang Line Required

Every CGI script must start with a shebang (#!) line specifying the interpreter, unless cgi_interpreter is set. A missing shebang causes a 500 Internal Server Error.

Migrating from Apache mod_cgi to NGINX

If you are moving from Apache to NGINX, the migration is straightforward. The NGINX CGI module supports the same environment variables as Apache’s mod_cgi, following the RFC 3875 standard.

Apache ScriptAlias to NGINX

Apache configuration:

ScriptAlias /cgi-bin/ /var/www/cgi-bin/
<Directory "/var/www/cgi-bin">
    AllowOverride None
    Options +ExecCGI
    Require all granted
</Directory>

Equivalent NGINX configuration:

location /cgi-bin {
    alias /var/www/cgi-bin;
    cgi on;
}

The NGINX alias directive maps /cgi-bin/hello.sh to /var/www/cgi-bin/hello.sh, just like Apache’s ScriptAlias.

Apache AddHandler to NGINX

Apache configuration (run all .py files as CGI):

AddHandler cgi-script .py

Equivalent NGINX configuration:

location ~ \.py$ {
    cgi on;
    cgi_interpreter /usr/bin/python3;
}

Apache SetEnv to NGINX

Apache configuration:

SetEnv APP_ENV production
SetEnv DB_HOST db.example.com

Equivalent NGINX configuration:

location /cgi-bin {
    cgi on;
    cgi_set_var APP_ENV $host;
    cgi_set_var DB_HOST $server_name;
}

Environment Variable Mapping

Both Apache mod_cgi and the NGINX CGI module implement the standard CGI environment variables. Your scripts will receive the same variables:

Variable Description
REQUEST_METHOD GET, POST, PUT, DELETE, etc.
QUERY_STRING URL query parameters
CONTENT_TYPE Request body content type
CONTENT_LENGTH Request body length
SCRIPT_NAME Path to the CGI script
SCRIPT_FILENAME Full filesystem path to the script
PATH_INFO Extra path after the script name
DOCUMENT_ROOT Web root directory
SERVER_NAME Server hostname
SERVER_PORT Server listening port
SERVER_SOFTWARE NGINX version string
REMOTE_ADDR Client IP address
REMOTE_PORT Client port
REQUEST_URI Original request URI
REQUEST_SCHEME http or https
GATEWAY_INTERFACE Always CGI/1.1
HTTP_* All HTTP request headers

Additionally, the module supports AUTH_TYPE and REMOTE_USER when NGINX authentication modules are enabled.

Advanced Configuration

Clean URLs with cgi_pass and PATH_INFO

The cgi_pass directive is one of the most powerful features of the NGINX CGI module. It routes all requests in a location to a single handler script, enabling clean URLs without file extensions or rewrite rules. The original request URI is passed to the script via the PATH_INFO environment variable.

This pattern is ideal for building simple REST-style APIs:

location /api {
    cgi_pass /var/www/cgi-scripts/api.sh;
}

The handler script uses PATH_INFO to determine the endpoint:

#!/bin/bash
echo "Content-Type: application/json"
echo
case "$PATH_INFO" in
  /api/users)
    echo '{"endpoint": "users", "method": "'"$REQUEST_METHOD"'"}'
    ;;
  /api/status)
    echo '{"status": "ok", "server": "'"$SERVER_SOFTWARE"'"}'
    ;;
  *)
    echo '{"error": "unknown endpoint"}'
    ;;
esac

Now requests to /api/users and /api/status are handled by the same script with no file extensions in the URL. You can test it:

curl http://localhost/api/status
# {"status": "ok", "server": "nginx/1.28.2"}

curl -X POST http://localhost/api/users
# {"endpoint": "users", "method": "POST"}

With traditional CGI mode (cgi on), PATH_INFO works differently. A request to /cgi-bin/router.sh/users/123?format=json automatically splits into SCRIPT_NAME=/cgi-bin/router.sh and PATH_INFO=/users/123, enabling clean sub-paths without rewrites.

Real-Time Streaming

The NGINX CGI module supports bidirectional streaming for both request and response bodies. This makes it possible to build interactive tools over HTTP. For example, here is a streaming calculator using bc:

#!/bin/bash
echo ""
bc 2>&1

Configure NGINX:

location /cgi-bin {
    cgi on;
}

Test it interactively with curl:

echo -e "2+3\n10*5\nquit" | curl -s -T - http://localhost/cgi-bin/calc.sh

Output:

5
50

The module automatically selects the correct transfer method — chunked encoding for HTTP/1.1 or streaming for HTTP/1.0. This makes CGI suitable for real-time log tailing, interactive shells, and other streaming use cases.

Webhook Handlers with Background Processing

CGI is an excellent fit for webhook endpoints that need to accept a request quickly and process it in the background. The key is to close stdout before starting the background task so that the HTTP response completes immediately:

#!/bin/bash
echo "Content-Type: application/json"
echo
echo '{"status": "accepted", "task_id": "12345"}'

# Start background task with all FDs closed
/path/to/process-webhook.sh </dev/null >/var/log/webhook.log 2>&1 &

The HTTP response returns in milliseconds while the background task continues running. This pattern works well for CI/CD triggers, notification handlers, and event processing.

Important: If a background process inherits the CGI script’s stdout pipe, the HTTP request will hang until that process exits. Always redirect stdin, stdout, and stderr before backgrounding.

System Administration Dashboards

CGI is a natural fit for simple server monitoring dashboards. Here is a system information script that combines several standard utilities:

#!/bin/bash
echo "Content-Type: text/plain"
echo
echo "=== Disk Usage ==="
df -h /
echo
echo "=== Memory ==="
free -h
echo
echo "=== Uptime ==="
uptime

Access it at `http://your-server/cgi-bin/sysinfo.sh` for an instant server health overview. You can build on this pattern for service status checks, log viewers, and configuration tools.

For even simpler cases, cgi_body_only lets you expose any existing command directly:

location /system-info {
    cgi_pass /usr/bin/uname -a;
    cgi_body_only on;
}

Privileged Execution with sudo

System administration CGI scripts often need elevated privileges. Instead of running NGINX as root, use sudo with tightly scoped rules. Create a sudoers file at /etc/sudoers.d/nginx-cgi:

# Allow nginx to run specific scripts with root privileges
nginx ALL=(root) NOPASSWD: /var/www/cgi-bin/service-control.sh

Then use cgi_interpreter to invoke sudo:

location /admin/service-control {
    cgi_pass /var/www/cgi-bin/service-control.sh;
    cgi_interpreter /usr/bin/sudo;
}

Alternatively, to allow all scripts in a directory to run with sudo:

location /admin {
    cgi on;
    cgi_interpreter /usr/bin/sudo;
}

Security warning: Always restrict sudo rules to specific scripts. Never grant blanket root access to all CGI scripts.

Running Python CGI Scripts

To run Python scripts without requiring a shebang line or execute permission, use cgi_interpreter:

location /python-cgi {
    cgi on;
    cgi_interpreter /usr/bin/python3;
}

Example Python CGI script at /var/www/html/python-cgi/info.py:

import os
import sys

print("Content-Type: text/plain")
print()
print(f"Python version: {sys.version}")
print(f"Server: {os.environ.get('SERVER_SOFTWARE', 'unknown')}")
print(f"Method: {os.environ.get('REQUEST_METHOD', 'unknown')}")
print(f"Gateway: {os.environ.get('GATEWAY_INTERFACE', 'unknown')}")

Running Perl CGI Scripts

Similarly, for Perl CGI scripts:

location /perl-cgi {
    cgi on;
    cgi_interpreter /usr/bin/perl;
}

Setting Process Timeouts

Use cgi_timeout to prevent runaway CGI processes from consuming resources:

location /cgi-bin {
    cgi on;
    cgi_timeout 30s 5s;
}

This sends a TERM signal after 30 seconds. If the process is still running after an additional 5 seconds, a KILL signal is sent. Without this directive, CGI processes run indefinitely.

Redirecting Stderr

By default, CGI script stderr output is captured and written to the NGINX error log. For better performance or separate log management, redirect stderr to a file:

location /cgi-bin {
    cgi on;
    cgi_stderr /var/log/nginx/cgi-stderr.log;
}

To discard stderr entirely:

location /cgi-bin {
    cgi on;
    cgi_stderr /dev/null;
}

Custom Environment Variables

Pass additional environment variables to CGI scripts using cgi_set_var. The value supports NGINX variables:

location /cgi-bin {
    cgi on;
    cgi_set_var CUSTOM_HOST $host;
    cgi_set_var CLIENT_IP $remote_addr;
    cgi_set_var REQUEST_ID $request_id;
}

Sandboxing with Docker

For enhanced isolation, the NGINX CGI module supports running scripts inside Docker containers via cgi_interpreter. First, create a persistent container with the document root mounted:

docker run -dit --restart always --name my_cgi \
    -v /var/www:/var/www busybox sh

Then configure NGINX to execute CGI scripts inside the container:

location /cgi-bin {
    cgi on;
    cgi_interpreter /usr/bin/docker exec my_cgi;
}

Every CGI request now runs inside the container with full filesystem and process isolation. This is a modern security pattern that Apache’s mod_cgi never offered natively.

Complete Directive Reference

Here is the complete list of directives provided by the NGINX CGI module:

cgi

Enables or disables CGI processing.

  • Syntax: cgi on | off or cgi pass <script_path> [args...]
  • Default: off
  • Context: server, location

When set to on, the module operates in traditional CGI mode — it locates the script under the document root based on the request URI. When using cgi pass, requests are routed to the specified script directly.

cgi_pass

Alias for cgi pass <script_path>.

  • Syntax: cgi_pass <script_path> [args...]
  • Context: server, location

cgi_interpreter

Sets the interpreter for executing CGI scripts.

  • Syntax: cgi_interpreter <path> [args...]
  • Default: (empty — scripts execute directly)
  • Context: server, location

When set, scripts are executed through the specified interpreter. This eliminates the need for shebang lines and execute permissions. Supports NGINX variables. This single directive replaces multiple Apache mechanisms including suexec, Action, and ScriptInterpreterSource.

cgi_working_dir

Sets the working directory for CGI script execution.

  • Syntax: cgi_working_dir <path>
  • Default: (empty — inherits NGINX working directory)
  • Context: server, location

Supports NGINX variables. Note that this does not affect how the interpreter or script is located — they are always resolved relative to the NGINX working directory.

cgi_body_only

Treats all script output as the response body.

  • Syntax: cgi_body_only on | off
  • Default: off
  • Context: server, location

When enabled, the module skips CGI header parsing. This is useful for running ordinary programs as CGI handlers without modifying their output format.

cgi_path

Sets the PATH environment variable for CGI scripts.

  • Syntax: cgi_path <path>
  • Default: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  • Context: server, location

cgi_strict

Enables or disables strict header validation.

  • Syntax: cgi_strict on | off
  • Default: on
  • Context: server, location

When enabled, malformed CGI headers cause a 500 Internal Server Error. When disabled, invalid headers are forwarded to the client. It is recommended to keep this enabled.

cgi_set_var

Passes custom environment variables to CGI scripts.

  • Syntax: cgi_set_var <name> <value>
  • Context: server, location

Can be specified multiple times to set multiple variables. The value supports NGINX variables. This directive can also override standard CGI variables.

cgi_stderr

Redirects CGI script stderr to a file.

  • Syntax: cgi_stderr <path>
  • Default: (captured to NGINX error log)
  • Context: server, location

Set to /dev/null to discard stderr, or /dev/stderr to redirect to the NGINX process stderr.

cgi_rdns

Enables reverse DNS lookup for the client hostname.

  • Syntax: cgi_rdns off | on | double [required]
  • Default: off
  • Context: server, location

When set to on, a reverse DNS lookup populates the REMOTE_HOST environment variable. When set to double, a forward DNS verification is performed after the reverse lookup. Adding required returns an error if the lookup fails. Requires a resolver directive to be configured. Enabling this adds latency to every request.

cgi_timeout

Sets timeout limits for CGI processes.

  • Syntax: cgi_timeout <term_time> [kill_time]
  • Default: 0 0 (disabled)
  • Context: server, location

Sends a TERM signal after term_time. If kill_time is specified and non-zero, sends a KILL signal after an additional kill_time if the process is still running.

Handling POST Requests and Request Bodies

CGI scripts receive POST data through stdin. Here is a Bash example that reads and echoes the request body:

#!/bin/bash
echo "Content-Type: text/plain"
echo
echo "Content-Length: $CONTENT_LENGTH"
echo "Content-Type: $CONTENT_TYPE"
echo "Body:"
cat

Test with:

curl -X POST -d "key=value&foo=bar" http://localhost/cgi-bin/post.sh

Tip for large files: CGI streaming cannot handle caching, range requests, or download resumption. For serving large files from a CGI script, return a 302 redirect to a static file URL instead of streaming the file through stdout.

Security Best Practices

Running CGI scripts requires careful security considerations. For comprehensive NGINX security hardening, see our guide on NGINX security headers.

Restrict CGI Locations

Only enable CGI in specific, controlled directories:

# Good: specific directory
location /cgi-bin {
    cgi on;
}

# Bad: enabling CGI site-wide
location / {
    cgi on;  # Don't do this!
}

File Permissions

CGI scripts must be executable, but the NGINX worker process (typically running as the nginx user) must have permission to execute them. Set ownership and permissions carefully:

chown root:nginx /var/www/html/cgi-bin/*.sh
chmod 750 /var/www/html/cgi-bin/*.sh

SELinux Considerations

On RHEL-based systems with SELinux enabled, you may need to set the correct security context:

chcon -R -t httpd_sys_script_exec_t /var/www/html/cgi-bin/

Or allow NGINX to execute CGI scripts via SELinux booleans:

setsebool -P httpd_enable_cgi on

Use Timeouts

Always set cgi_timeout to prevent resource exhaustion from misbehaving scripts:

location /cgi-bin {
    cgi on;
    cgi_timeout 30s 5s;
}

Avoid Running as Root

Never configure CGI scripts to run with elevated privileges globally. If a script requires root access, use sudo with a tightly scoped sudoers rule as shown in the privileged execution section above.

Performance Considerations

CGI spawns a new process for every request, which adds significant overhead compared to FastCGI or application servers. Here are some guidelines:

  • Low traffic (< 10 requests/minute): CGI is perfectly fine. The overhead is negligible for admin panels, webhooks, and management scripts.
  • Moderate traffic (10–100 requests/minute): CGI works but consider migrating to FastCGI for better response times.
  • High traffic (> 100 requests/minute): Do not use CGI. Use FastCGI, uWSGI, or a dedicated application server.

Monitoring CGI Performance

Monitor CGI script execution times by checking the NGINX access log. Add the $request_time variable to your NGINX log format:

log_format cgi_log '$remote_addr [$time_local] "$request" '
                   '$status $body_bytes_sent $request_time';

server {
    location /cgi-bin {
        cgi on;
        access_log /var/log/nginx/cgi-access.log cgi_log;
    }
}

Troubleshooting

500 Internal Server Error

The most common cause is a missing or incorrect shebang line. Verify that:

  1. The script starts with #!/bin/bash (or the appropriate interpreter)
  2. The shebang line uses the correct path — check with which bash
  3. The script has a blank line separating headers from the body
  4. The script file has no Windows-style line endings (\r\n)

Convert line endings if needed:

dos2unix /var/www/html/cgi-bin/script.sh

403 Forbidden

This usually means permission issues:

  1. Check that the script has execute permission: chmod +x script.sh
  2. Verify the NGINX user can access the directory: namei -l /var/www/html/cgi-bin/script.sh
  3. On RHEL-based systems, check SELinux: ausearch -m avc -ts recent

Request Hangs

If a CGI request never completes, a child process has inherited the stdout pipe. Background processes spawned by CGI scripts must close stdin, stdout, and stderr:

#!/bin/bash
echo "Content-Type: text/plain"
echo
echo "Task started"

# Correct: redirect all FDs before backgrounding
/path/to/long-task.sh </dev/null >/var/log/task.log 2>&1 &

Use cgi_timeout as a safety net against hanging processes.

Module Not Loaded

If NGINX reports unknown directive "cgi", the module is not loaded. Verify:

  1. The module file exists: ls /usr/lib64/nginx/modules/ngx_http_cgi_module.so
  2. The load_module directive is at the top of nginx.conf, before the http {} block
  3. Run nginx -t to test the configuration

Complete Example: Apache to NGINX Migration

Here is a complete example migrating a typical Apache CGI setup to NGINX.

Original Apache configuration:

ServerName example.com
DocumentRoot /var/www/html

ScriptAlias /cgi-bin/ /var/www/cgi-bin/
<Directory "/var/www/cgi-bin">
    AllowOverride None
    Options +ExecCGI
    Require all granted
</Directory>

<Directory "/var/www/html/scripts">
    Options +ExecCGI
    AddHandler cgi-script .py .pl
</Directory>

Equivalent NGINX configuration:

load_module modules/ngx_http_cgi_module.so;

server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    # Migrate ScriptAlias /cgi-bin/
    location /cgi-bin {
        alias /var/www/cgi-bin;
        cgi on;
        cgi_timeout 30s 5s;
    }

    # Migrate AddHandler for .py and .pl files
    location ~ \.(py|pl)$ {
        cgi on;
        cgi_timeout 30s 5s;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

Conclusion

The NGINX CGI module provides a reliable way to run traditional CGI scripts in NGINX. It is particularly valuable when migrating legacy applications from Apache, where rewriting scripts for FastCGI is impractical. The cgi_interpreter directive alone replaces multiple Apache mechanisms — suexec, Action, and ScriptInterpreterSource — while adding modern capabilities like Docker sandboxing.

For new projects, consider using FastCGI or an application server instead. However, for admin tools, legacy systems, embedded devices, webhook handlers, and prototyping, CGI with NGINX is a practical and supported solution.

The module source code is available on GitHub. For RPM packages, visit the GetPageSpeed module page.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.