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_moduledirective 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:
- Header section — HTTP response headers, one per line
- 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 | offorcgi 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
302redirect 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:
- The script starts with
#!/bin/bash(or the appropriate interpreter) - The shebang line uses the correct path — check with
which bash - The script has a blank line separating headers from the body
- 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:
- Check that the script has execute permission:
chmod +x script.sh - Verify the NGINX user can access the directory:
namei -l /var/www/html/cgi-bin/script.sh - 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:
- The module file exists:
ls /usr/lib64/nginx/modules/ngx_http_cgi_module.so - The
load_moduledirective is at the top ofnginx.conf, before thehttp {}block - Run
nginx -tto 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.

