Contact Us

Running CGI Scripts in Angie

The web started out with CGI scripts. They were the foundation of web programming in the early days of web applications. Today CGI has been replaced by more performant, modern ways of running server-side code, but old faithful CGI is still going strong and can solve certain tasks. Did you know that Angie can run CGI scripts without proxying?

Video version

A video version of this article is available on YouTube.

What is CGI?

Common Gateway Interface (CGI) is an interface for running program code on a server over HTTP(S). Usually this means scripts — programs in interpreted languages — but any compiled program that follows the relevant specification can be used as well.

The interaction logic is as simple as it gets: all user data reaches the program's input as environment variables and standard input (STDIN), and the program's output (STDOUT) is sent back to the client as the response. Some environment variables are derived from request properties (REMOTE_ADDR, REQUEST_METHOD, SCRIPT_NAME, and so on; GET parameters land in the QUERY_STRING variable).

Almost any scripting language that runs on the server can be used for CGI: Bash, Perl, PHP, Python, and others.

Running a Bash script through CGI

The traditional way to work with CGI scripts is to install the Apache web server and its standard CGI module. Requests with scripts are then proxied from Angie to Apache. For simple tasks, though, that is an over-engineered and clunky solution.

CGI script execution in Angie is provided by a third-party CGI module; full documentation for the module lives on the project page.

Install the module from the Angie repository:

apt install angie-module-cgi

Load it at the top of the main angie.conf config:

load_module modules/ngx_http_cgi_module.so;

Now create a test Bash script (say, one that prints environment variables) at /var/www/html/cgi/test.sh:

#!/bin/sh
echo "Content-Type: text/plain" # Add a header to the response
echo "" # Separator between headers and body

# Environment variables
echo "query string: $QUERY_STRING"
echo "server addr: $SERVER_ADDR"
echo "server port: $SERVER_PORT"

# Request headers via environment variables
echo "http host: $HTTP_HOST"
echo "http accept: $HTTP_ACCEPT"
echo "http Some-Field: $HTTP_SOME_FIELD"

body=$(cat) # Reads the request body into a variable
echo "Request body: $body"

Do not forget to make the script executable.

chmod +x /var/www/html/cgi/test.sh

As the example shows, the script can read environment variables that are populated from request headers and other HTTP request parameters. All that's left is to wire up CGI script handling for the /var/www/html/cgi directory. For that, create a server with a /cgi/ location:

server {
listen 80;
root /var/www/html;
    location /cgi/ {
            cgi on;
    }
}

The only directive from the CGI module is cgi on in the /cgi/ location. When you use CGI scripts, be very careful about the security of the directories where scripts are allowed to run. You can keep such directories outside the web roots to make it harder for malicious code to end up there. Data from users in these scripts can't be trusted either, so validate every incoming parameter.

Let's check the script with curl. The first request has no body and no parameters:

curl http://localhost/cgi/test.sh

query string:
server addr: 127.0.0.1
server port: 80
http host: localhost
http accept: */*
http Some-Field:
Request body:

The second request carries a GET parameter test_var and a request body (JSON).

curl http://localhost/cgi/test.sh?test_var=test -d '{"key1":"value1", "key2":"value2"}'

query string: test_var=test
server addr: 127.0.0.1
server port: 80
http host: localhost
http accept: */*
http Some-Field:
Request body: {"key1":"value1", "key2":"value2"}

As you can see, the script picks up the request's GET parameters through the $QUERY_STRING variable and also gets access to the request body through STDIN (standard input).

Let's look at how more powerful scripting languages work in CGI mode, using Perl as the example.

Perl script example

The Perl interpreter is most likely already installed on your system. If not, install it (the example is for Ubuntu); we also need the CGI module to work with CGI parameters. Install everything in one command:

apt install perl libcgi-pm-perl

Now create a small script that reads request parameters and displays the environment. Put it next to the Bash script (/var/www/html/cgi/test.pl). Don't forget the read and execute permissions:

#!/usr/bin/perl

use v5.10;
use CGI;

my $cgi = CGI->new();
print $cgi->header;

my $test = $cgi->param('test');

print qq{
<!doctype html>
<html><h1>Test Perl content!</h1>
<p>Test param: $test</p>
<p>ENV UA: $ENV{HTTP_USER_AGENT}</p>
<p>Whole $ENV</p>};

foreach my $key (keys %ENV) {
    print "<p>ENV $key: $ENV{$key}</p>\n";
}

print qq{<html>};

The script first enables the modern Perl mode (use v5.10) and loads the CGI.pm module (use CGI). Next we create the $cgi object to work with request parameters. CGI.pm in general has a wide feature set, but it is recommended to use it only for parameters (GET and POST) and headers. Then we print a proper HTTP response header by calling the header method of the $cgi object.

The next step stores the value of the test parameter in the $test variable. To demonstrate it in a browser, we build an HTML page that prints the parameter and an environment variable from the %ENV hash. Finally, a foreach loop prints every key and value of the %ENV hash.

Perl's text-processing capabilities are extensive, and there are also hundreds of thousands of additional modules in the CPAN (Comprehensive Perl Archive Network) repository covering ready-made solutions for typical tasks: database work, processing files in various formats, image handling, math libraries, and more.

The error messages from the CGI module for Angie leave something to be desired. For instance, when the CGI.pm module is missing from the system we get a 500 response code (which is expected) and this message in the error log:

2025/11/24 16:34:35 [error] 4572#4572: *23 ngx_http_cgi_deref_process, status = 2 (11: Resource temporarily unavailable), client: 127.0.0.1, server: _, request: "GET /cgi/test.pl HTTP/1.1", host: "localhost"

The actual cause of the error is nowhere to be seen in that message.

More CGI module features

Above we wired up script execution for an entire location. Sometimes you need to hide the script's address and narrow down what can be launched. The cgi_pass directive is useful here. One use case is rendering Markdown documents. The location configuration:

location ~ \.md$ {
  cgi_pass /var/www/html/cgi/render-markdown.sh;
}

The script handles requests ending in .md, while the user requests the address of the .md file rather than the script. The script learns which document was requested from environment variables. An example script:

#!/bin/sh

set -e

if [ ! -f "${DOCUMENT_ROOT}${PATH_INFO}" ]; then
    echo "Status: 404"
    echo
    exit
fi

echo "Status: 200"
echo "Content-Type: text/html"
echo

echo "<html><body>"
markdown "${DOCUMENT_ROOT}${PATH_INFO}"
echo "</body></html>"

Requests to md files now return the document rendered as HTML.

But what if a script needs to run as root? By default, scripts run as the user that owns the Angie worker processes (for example, www-data or angie). Running as root can be done through the sudo mechanism (if your system uses it). Create a rule that lets the web server user (in our case www-data) use sudo by adding an entry to /etc/sudoers:

www-data ALL=(root) NOPASSWD: /var/www/html/bin/my-danger-script

Here we allow exactly one script to run as root (/var/www/html/bin/my-danger-script). Now update the location configuration:

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

In the example above we use the cgi_interpreter directive to change how the script is launched, so the script runs as root. The directive can also be used to run scripts without the execute bit, launch binaries in a CGI environment, filter script output, and so on.

The CGI module lets you fine-tune the conditions under which scripts run, restrict scripts with chroot, and configure the environment. See the module documentation for details.

Summary

For narrow, specific tasks CGI scripts can still be useful, and we now know how to run them directly in Angie with a third-party module. We also went through a few Bash and Perl script examples covering the standard ways of passing data to CGI.