Serve CGI Scripts through fcgiwrap and Nginx on Debian
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. You need root access for package and service changes; the diagnostic commands can usually run as an ordinary user.
The route
Jump straight to the step you need, or tick off Done means at the end.
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 and has 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
Expect the socket unit to be active after it has been started. The stat command should show srw-rw---- and www-data:www-data on this installation. If the socket does not exist, start the socket 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. To undo only that enablement, use 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 that is 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, starting with headers such as Content-Type and followed by a blank line. 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, a minimal shell CGI could be installed by an administrator 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'
The install command 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 after verification with rm -- /usr/lib/cgi-bin/example.cgi only when you are certain it is not used by another site.
Checkpoint: confirm the script before configuring Nginx
At this point you should have an executable CGI path and an active socket. If either is missing, stop here. 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 gives 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 that SCRIPT_FILENAME overrides the DOCUMENT_ROOT plus SCRIPT_NAME calculation. That is useful here because the filesystem path is explicit. Do not copy this location into a server that exposes a different CGI directory without changing both the URL and 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
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
The exact HTTP headers vary with Nginx. The useful checks are a successful response, the CGI content type, and the body produced by the script. 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 prevents fcgiwrap from rewriting that value while finding 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. Do not solve that 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. 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
fcgiwrap.socketis active and/run/fcgiwrap.socketis not world-writable.- The CGI file is reviewed, executable, and mapped to the requested URL deliberately.
nginx -tpassed before the live reload.- A test request returned the expected CGI headers and body.
- You know where to check Nginx and fcgiwrap logs without weakening socket permissions.