Danny's user image
User since: Nov 03, 2014 at 19:05 UTC (11 years ago)
Last here: Apr 09, 2025 at 10:12 UTC (26 weeks ago)
Experience: 2226
Level:Chaplain (11)
Writeups: 211
Location:Indiana
User's localtime: Oct 05, 2025 at 05:31 UTC
Scratchpad: View
For this user:Search nodes

Below is a method I use to implement a fast web server using Perl with Nginx on a Linux system. I have ran three servers for several years with this and haven't had any issues. I'm posting it here for three reasons: 1) It may be of use to someone. 2) Someone will probably tell me a better way to do it. 3) It will help me remember how to do it if I need to reproduce it.

The perl s‎crip‎t below is the daemon that runs via systemd. It is started by the systemd service file (/usr/lib/systemd/system/myserver.service) that looks something like:

[Unit] Des‎crip‎tion=Myserver CGI Server After=nss-user-lookup.target network.target nss-lookup.target sockets. +target # possibly mount services Requires=myserver.socket [Service] Type=simple RuntimeDirectory=www-data RuntimeDirectoryPreserve=yes User=www-data Group=www-data ExecStart=/var/www/some/more/paths/Myserver.fcgi # Or wherever you wan +t to put it StandardInput=socket [Install] WantedBy=multi-user.target Also=myserver.socket

where /var/www/some/more/paths/Myserver.fcgi is the Perl s‎crip‎t below. "myserver" etc should be changed to whatever you want to call things. This service is run automatically by linking via /etc/systemd/system/multi-user.target.wants/. The myserver.socket service file looks something like:

[Unit] Des‎crip‎tion=Myserver Socket [Socket] SocketUser=www-data SocketGroup=www-data ListenStream=/run/www-data/Myserver.fcgi.socket [Install] WantedBy=sockets.target

On the Nginx side, the relevant parts of the main server config look like:

upstream myserver { server unix:/run/www-data/Myserver.fcgi.socket; keepalive 2; } server { # ... root /var/www/some_place; include /etc/nginx/fastcgi_params; location ~ ^/some_uniq_identifier/?$ { include myserver.conf; } # ... }

where myserver.conf looks like:

gzip off; fastcgi_pass myserver; # defined above with "upstream myserver" fastcgi_keep_conn on;
Perl s‎crip‎t, e.g. /var/www/some/more/paths/Myserver.fcgi
#!/usr/bin/perl -T

# This version uses FCGI and forks.  The children use CGI to process stdin.

use warnings;
use strict;
use POSIX ":sys_wait_h";
use FCGI;
use CGI ();
use DBI;
use lib ( $ENV{SOME_PATH} );
use Myserver;

delete $ENV{PATH};
my $DEBUG = 0;
my %FORKS;
my ($SOCKET, $PID_FILE, $debug_fh);
if($DEBUG) {
  $SOCKET = "/tmp/Myserver.debug.socket";
  $PID_FILE = "/tmp/Myserver.debug.pid";
  my $debug_log = "/tmp/Myserver.debug.log";
  open $debug_fh, ">", $debug_log or die "error: can't open file=$debug_log\n";
} else {
  $SOCKET = "/var/run/www-data/Myserver.fcgi.socket";
  $PID_FILE = "/var/run/www-data/Myserver.pid";
}
my $LISTEN_QUEUE = 50;
$SIG{CHLD} = 'IGNORE';
my %opts = ( note => "just an example" );
my $s = Myserver->new( \%opts );
init_Myserver( $s ); # If needed: set up database connections, constants etc
$s->writePidFile( $PID_FILE ); # Optional: If you want to keep a file with the daemon process id.

my $socket = FCGI::OpenSocket($SOCKET, $LISTEN_QUEUE);
my $req = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket);

my $count = 0;
while ($req->Accept() >= 0) {
  $count++;
  $DEBUG and printf $debug_fh "\n%s FCGI accepted request count=$count ppid=$$\n", scalar localtime;
  my $pid = fork;
  if( not defined $pid ) {
    $s->{CGI} = CGI->new;
    $s->error("Child failed to fork in Myserver.fcgi.  Please email me\@somewhere.com if you get this error.\n");
    next;
  }
  if($pid) {
    $req->Detach; # So next Accept doesn't disconnect current request that fork is handling
    $DEBUG and printf $debug_fh "%s Spawned child cpid=$pid\n", scalar localtime;
    $DEBUG and printf $debug_fh "%s Detached from request.  Waiting for next request.\n", scalar localtime;
    $DEBUG and $debug_fh->flush;
    checkForks();
    $FORKS{$pid} = time;
    next; # wait for next request
  }
  $s->{CGI} = CGI->new;
  $s->check_dbh or child_exit($req);
  $s->{CALL_COUNT} = $count; # Optional: This is for debugging. You can insert an attribute in your HTML to show the call count or do whatever you want with it.
  $s->init_Myserver_per_request; # Optional: Per request as opposed to the one time init_Myserver( $s ) above.
  $s->ProcessRequest(); # Do the work on the request.
  child_exit($req);
}

sub child_exit {
  my $req = shift;
  $req->Flush;
  $req->Finish;
  exit; # kill the child
}

# This is for cleaning up forks that may accumulate because of network issues,
# etc. Some debug statements can be added to check if this may be happening.
sub checkForks {
  # Kill forks older than a minute old
  my $t = time;
  foreach my $pid ( keys %FORKS ) {
    if(waitpid($pid, WNOHANG) != 0) {
      delete $FORKS{$pid};
      next;
    }
    $t - $FORKS{$pid} > 60 or next;
    # printf $old_child_fh "%s Child lived more than a minute; might want to investigate.\n", scalar localtime;
    kill 'KILL', $pid;
    delete $FORKS{$pid};
  }
}

# This is called when daemon is started, but not when URL requests are being
# made, so it's OK to die here if we can't connect to database.
sub init_Myserver { # one time init
  my $s = shift;
  $s->{VAR} = "some important variable";
  $s->{DBH} = $s->DBIconnect(); # If needed
  # etc
}