Serve CGI Scripts through fcgiwrap and Nginx on Debian
Nginx cannot run CGI on its own, so fcgiwrap is the small bridge that runs your old scripts for it. You will connect Nginx to Debian's systemd-managed fcgiwrap socket, point it at a CGI directory and verify the request with a harmless script. Allow about 20 minutes if Nginx is already installed and you can reload its configuration.
The route
Jump straight to the step you need, or tick off Done means at the end.
- Root needed for package and service changes.
- Ordinary user is enough for most of the diagnostic commands.
1. Check the installed fcgiwrap and socket
This guide is based on Debian's fcgiwrap package 1.1.0-14build1, whose executable reports fcgiwrap 1.1.0. The package supplies a systemd socket unit listening on /run/fcgiwrap.socket. The socket is owned by www-data:www-data with mode 0660, so the Nginx worker must be able to access it.
$ fcgiwrap -h
Usage: fcgiwrap [OPTION]
Invokes CGI scripts as FCGI.
$ systemctl status fcgiwrap.socket --no-pager
$ stat -c '%A %U:%G %n' /run/fcgiwrap.socket
Checkpoint
The socket unit should be active once started, and stat should show srw-rw---- and www-data:www-data on this installation.
If the socket does not exist, start it before touching Nginx:
# systemctl enable --now fcgiwrap.socket
# systemctl is-active fcgiwrap.socket
active
This enables socket activation for future boots and starts it now.
Recovery
To undo only that enablement, run systemctl disable --now fcgiwrap.socket during a maintenance window. Stopping it will break requests that depend on this socket.
2. Put one known CGI script in place
Use a directory deliberately separate from ordinary static files. Debian's example configuration maps the URL prefix /cgi-bin/ to /usr/lib/cgi-bin. Check whether that directory already contains the script you intend to run:
$ sudo find /usr/lib/cgi-bin -maxdepth 1 -type f -printf '%M %u:%g %p\n'
$ sudo test -x /usr/lib/cgi-bin/example.cgi && echo executable
Replace example.cgi with the real filename. A CGI program must be executable and should emit a valid CGI response: headers such as Content-Type, then a blank line.
Warning
Do not make an unreviewed file executable in a web-facing directory. fcgiwrap runs CGI programs on behalf of incoming requests.
For a controlled smoke test, an administrator can install a minimal shell CGI after reviewing it:
# install -o root -g www-data -m 0750 /path/to/reviewed-example.cgi /usr/lib/cgi-bin/example.cgi
$ head -n 8 /usr/lib/cgi-bin/example.cgi
#!/bin/sh
printf 'Content-Type: text/plain\n\n'
printf 'fcgiwrap test OK\n'
Warning
install changes system state and overwrites the destination if it already exists. Preserve an existing file first, or choose a new name. Remove the test script afterwards with rm -- /usr/lib/cgi-bin/example.cgi, and only when you are certain no other site uses it.
Checkpoint
Before configuring Nginx you should have an executable CGI path and an active socket. If either is missing, stop here, because Nginx errors cannot repair a missing script or an inaccessible socket.
3. Add the Nginx CGI location
Inside the relevant server block, add a location like this. It follows Debian's installed example and uses SCRIPT_FILENAME, which hands fcgiwrap the complete executable path and avoids ambiguity around PATH_INFO.
location /cgi-bin/ {
gzip off;
root /usr/lib;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib$fastcgi_script_name;
fastcgi_pass unix:/run/fcgiwrap.socket;
}
With this mapping, a request for /cgi-bin/example.cgi produces the path /usr/lib/cgi-bin/example.cgi. The manual says SCRIPT_FILENAME overrides the DOCUMENT_ROOT plus SCRIPT_NAME calculation, which is handy here because the filesystem path is explicit.
Tip
Do not copy this location into a server that exposes a different CGI directory without changing both the URL and the filesystem mapping.
Test the complete Nginx configuration before reloading it:
$ sudo nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful
# systemctl reload nginx
Recovery
A reload normally keeps existing connections alive, but it still changes live service behaviour. If the test fails, do not reload. Restore the previous configuration or remove only the new location, then rerun nginx -t.
4. Make a request and check the result
Use the public hostname and URL that select the server block you edited. A local request is less surprising when the site is bound locally:
$ curl --include http://127.0.0.1/cgi-bin/example.cgi
HTTP/1.1 200 OK
Content-Type: text/plain
fcgiwrap test OK
Checkpoint
The exact HTTP headers vary with Nginx. What matters is a successful response, the CGI content type and the body the script produced.
If the script accepts a path suffix, a URL such as /cgi-bin/example.cgi/extra supplies the remainder as PATH_INFO only when the web server passes the parameters in a compatible way. Supplying SCRIPT_FILENAME stops fcgiwrap rewriting that value while it finds the executable.
5. Diagnose the common failures
A 502 Bad Gateway usually means Nginx cannot connect to the FastCGI endpoint, or the upstream closed the connection. Check the socket, unit logs and Nginx error log:
$ systemctl is-active fcgiwrap.socket
$ journalctl -u fcgiwrap.service -n 50 --no-pager
$ sudo tail -n 50 /var/log/nginx/error.log
If the socket is present but access is denied, compare its owner and mode with the Nginx worker identity.
Security warning
Do not fix this by making the socket world-writable. The package changed its socket permissions from a world-writable setting to 0660 after a security issue, so widening access can turn an access problem into a privilege-escalation risk.
A 404 from Nginx often means the location or URL does not match what you configured. A Permission denied message from fcgiwrap usually points to the script, one of its parent directories, or a missing execute bit. Check without changing anything:
$ namei -l /usr/lib/cgi-bin/example.cgi
$ test -x /usr/lib/cgi-bin/example.cgi; echo "script status: $?"
$ sudo journalctl -u fcgiwrap.service -n 50 --no-pager
With -f, fcgiwrap sends CGI standard error through FastCGI so it appears in the web server's error log. Debian's systemd service already sets DAEMON_OPTS=-f.
Tip
Avoid adding another conflicting service or manually binding the same socket with fcgiwrap -s. The packaged socket unit already opens the endpoint and passes it on file descriptor 0.
Done means
- Socket healthy.
fcgiwrap.socketis active and/run/fcgiwrap.socketis not world-writable. - Script vetted. The CGI file is reviewed, executable and mapped to the requested URL deliberately.
- Config tested.
nginx -tpassed before the live reload. - Request works. A test request returned the expected CGI headers and body.
- Logs known. You know where to check Nginx and fcgiwrap logs without weakening socket permissions.