« back published by @mmartin_joo on May 1, 2023

DevOps with Laravel: CGI, FastCGI, php-fpm, nginx

If you're running PHP/Laravel applications in production there's a good chance you're using some of these:

  • CGI
  • FastCGI
  • php-fpm
  • nginx

As a developer, I think it's important to at least understand the basics of these components. So let's dig in.

Serving static content with nginx

First, let's start with serving static content with nginx. Let's assume a pretty simple scenario where we have only three files:

  • index.html
  • style.css
  • logo.png

Each of these files live inside the /var/www/html/demo folder. There are no subfolders no PHP files.

# `events` is not important right now..
events {}

http {
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  include mime.types;

  server {
    listen 80;
    server_name 138.68.81.14;
    root /var/www/html/demo;
  }
}

This is not a production config! It's only for demo purposes.

In an nginx config there are two important terms, context, and directive:

Context is similar to a "scope" in a programming language. http, server, location, and events are the contexts in this config. They define a scope where we can configure scope-related things. http is global to the whole server. So if we have 10 sites on this server each will log to the /var/log/nginx/access.log file which is obviously not good, but it's okay for now.

Another context is server which refers to one specific site. In this case, the site is http://138.68.81.14. Inside the server context we can have location context (but we don't have right now) which refers to specific URLs on this site.

So with contexts we can describe the "hierarchy" of things:

http {
  # server-level
  
  server {
    # site-level
    
    location {
      # URL-level
    }
  }
}

Inside the contexts we can write directives. It's similar to a function invocation or a value assignment in PHP. listen 80; is a directive, for example. Now let's see what they do line-by-line.

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

nginx will log every incoming request to the access.log file in a format like this: 172.105.93.231 - - [09/Apr/2023:19:57:02 +0000] "GET / HTTP/1.1" 200 4490 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko"

When something goes wrong if logs it to the error.log file. One important thing though, a 404 or 500 response is not considered as error. The error.log file contains only nginx specific errors, for example, if it cannot be started because the config is invalid.

include mime.types;

Do you remember the good old include() function from PHP? The nginx include directive does the same thing. It loads another file. mime.types is a file located in the same directory as this config file (which is /etc/nginx). The content of this file looks like this:

types {
  text/html html htm shtml;
  text/css css;
  text/xml xml;
  # ...
}

As you can see it contains common mime types and file extensions. If we don't include these types nginx will send every response with the Content-Type: text/plain header and the browser will not load CSS and javascript properly. With this directive if I send a request for a CSS file nginx sends a response such as:

Laracheck

By the way, I didn't write mimes.type it comes with nginx by default.

Next up, we have the server-related configs:

listen 80;
server_name 138.68.81.14;

This configuration uses HTTP without SSL so it listens on port 80. The server_name usually is a domain name, but right I only have an IP address so I use that.

root /var/www/html/demo;

The root directive defines the root folder of the given site. Every filename after this directive will refer to this path. So if you write index.html it means /var/www/html/demo/index.html

By default, if a request such as this: GET http://138.68.81.14 comes in nginx will look for an index.html inside the root folder. Which, if you remember, exists so it can return it to the client.

When the browser parser the HTML and sends a request for the style.css the request looks like this: http://138.68.81.14/style.css which also exists since it lives inside the root folder.

That's it! This is bare minimum nginx configuration to serve static content. Once again, it's not production-ready and it's not optimized at all.

nginx doesn't know anything about PHP. If I add an index.php to the root folder and try to request it, I get the following response:

Laracheck

So it returns the content of the file as plain text. Let's fix this!

This whole article comes from my new 465-page book called DevOps with Laravel. In the book, I'm talking about:

  • Fundamentals such as nginx, CGI, FastCGI, FPM
  • Backup and restore scripts
  • Optimization
  • CI/CD pipelines
  • Log management and monitoring
  • Docker and docker-compose
  • Docker Swarm
  • Kubernetes
  • Serverless and PaaS
  • ...and more
DevOps with Laravel

CGI, FastCGI, php-fpm

As I said, nginx doesn't know how to run and interpret PHP scripts. And it's not only true for PHP it doesn't know what to do with a Ruby or Perl script either. So we need something that connects the web server with our PHP scripts. This is what CGI does.

CGI

CGI stands for Common Gateway Interface. As the name suggests, it's not a package or library. No, it's an interface, a protocol. The original specification defines CGI like this:

The Common Gateway Interface (CGI) is a simple interface for running external programs, software or gateways under an information server in a platform-independent manner. - CGI specification

CGI gives us a unified way to run scripts from web servers to generate dynamic content. It's platform and language-independent so the script can be written in PHP, python, or anything. It can even be a C++ or Delphi program it doesn't need to be a classic "scripting" language. It can be implemented in any language that supports network sockets.

CGI uses a "one-process-per-request" model. It means that when a request comes in to the web browser it creates a new process to execute the php script:

Laracheck

If 1000 requests come in it creates 1000 processes and so on. The main advantage of this model is that it's pretty simple but the disadvantage is that it's pretty resource intensive and hard to scale when there's a high traffic. The cost of creating and destroying processes is quite high. The CPU also needs to switch context pretty often which becomes a costly task when the load is big on the server.

FastCGI

FastCGI is also a protocol. It's built on top of CGI and as its name suggests it's faster. Meaning it can handle more load for us. FastCGI does this by dropping the "one-process-per-request" model. Instead, it has persistent processes which can handle multiple requests by its lifetime so it reduces the CPU overhead of creating/destroying processes and switching between them. It also implements multiplexing but this is not the topic of our discussion.

It looks something like that:

Laracheck

FastCGI can be implemented over unix sockets or TCP. We're going to use both of them later.

php-fpm

fpm stands for FastCGI Process Manager. php-fpm is not a protocol or an interface but an actual executable program. A Linux package. This is the component that implements the FastCGI protocol and connects nginx with our Laravel application.

It runs as a separate process on the server and we can instruct nginx to pass every PHP request to php-fpm which will run the Laravel app and return the HTML or JSON response to nginx.

It's a process manager so it's more than just a running program that can accept requests from nginx. It actually has a master process and many worker processes. When nginx sends a request to it the master process accepts it and forwards it to one of the worker processes. The master process is basically a load balancer that distribute the work across the workers. If something goes wrong with one of the workers (for example, exceeding max execution time or memory limit) the master process can kill and restart these processes. It can also scale up and down worker processes as the traffic increases or decreases. php-fpm also helps us avoid memory leaks since it will terminate and respawn worker process after a fixed number of requests.

By the way, this master-worker process architecture is pretty similar to how nginx works (we'll see more about that later).

nginx and PHP

With that knowledge we're now ready to handle PHP requests from nginx! First, let's install php-fpm:

apt-get install php-fpm

After the installation everything should be ready to go. It should also run a systemd service which you can check by running these commands:

systemctl list-units | grep "php"
systemctl status php8.0-fpm.service # in my case its php8.0

And the output should look like this:

Laracheck

Here's the nginx config that connects requests with php-fpm:

user www-data;

events {}

http {
  include mime.types;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  server {
    listen 80;
    server_name 138.68.79.28;

    root /var/www/html/demo;

    index index.php index.html;

    location / {
      try_files $uri $uri/ =404;
    }

    location ~\.php {
      include fastcgi.conf;
      fastcgi_pass unix:/run/php/php-fpm.sock;
    }
  }
}

Most of it should be familiar but there are some new directives. Now we have PHP files in our project so it's a good practice to add index.php as the index file:

index index.php index.html;

If it cannot be found nginx will default to an index.html.

Next we have this location scope:

location / {
  try_files $uri $uri/ =404;
}

try_files is an exception great name because it literally tries to load the given files in order. But what is $uri or =404

$uri is a variable given to us by nginx. It contains the normalized URI from the URL. Here's a few examples:

  • mysite.com -> /
  • mysite.com/index.html -> /index.html
  • mysite.com/posts/3 -> /posts/3
  • mysite.com/posts?sort_by=publish_at -> /posts

So if the request contains a specific filename nginx tries to load it. This is what the first part does:

try_files $uri

If the request is mysite.com/about.html then it returns the contents of about.html.

What if the request contains a folder name? I know it's not that popular nowadays (or in Laravel) but nginx was published a long time ago. The second parameter of try_files makes it possible to request a specific folder:

try_files $uri $uri/

For example, if the request is mysite.com/articles and we need to return the index.html from the articles folder the $uri/ makes is possible. This is what happens:

  • nginx tries to find a file called articles in the root but it's not found
  • Because of the / in the second parameter $uri/ it looks for a folder named articles. Which exists.
  • Since in the index directive we specified that the index file should be index.php or index.html it loads the index.html under the articles folder.

The third parameter is the fallback value. If neither a file or folder cannot be found nginx will return a 404 response:

try_files $uri $uri/ =404;

So the first location block takes of static content. The second one handles requests for PHP files. Remember, Laravel and fancy user-friendly URLs are not involved just yet. For now, a PHP request means something mysite.com/phpinfo.php

To catch these requests we need a location such as this:

location ~\.php {}

As you can see it's a regex since we want to match any PHP files:

  • ~ just means it's a regex and it's case-sensitive (~* is used for case-insensitive regexes)
  • \. is just the escaped version of the . symbol

So this will match for any PHP file.

Inside the location there's an include:

include fastcgi.conf;

As we already discussed nginx comes with some predefined configurations that we can use. This file basically defines some basic environment variables to php-fpm. Things like these:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;

php-fpm needs informations about the request method, query string, the file that's being executed and so on.

And the last line is where the magic happens:

fastcgi_pass unix:/run/php/php-fpm.sock;

This instructs nginx to pass the request to php-fpm through a Unix socket. If you remember, FastCGI can be used via Unix sockets or TCP connections. Here we're using the earlier. I don't know much about Unix sockets but they provide a way to pass binary data between processes. This is exactly what happens here.

Here's a command to locate the php-fpm socket's location:

find / -name *fpm.sock

It finds any file names *fpm.sock inside the / folder (everywhere on the server).

So this is the whole nginx configuration to pass requests to php-fpm:

location ~\.php {
  include fastcgi.conf;
  fastcgi_pass unix:/run/php/php-fpm.sock;
}

Later we'll do the same inside docker containers and with Laravel. We'll also talk about how to optimize nginx and php-fpm.

nginx and Vue

When you run npm run build it builds to whole frontend into static HTML, CSS, and javascript files that can be served as simple static files. After the browser loads the HTML it sends requests to the API. Since this is the case serving a Vue application doesn't require as much as serving a PHP API.

However, in the "Serving static content" I showed you a pretty basic config for demo purposes so here's a better one:

server {
  listen 80;
  server_name 138.68.80.16;
  root /var/www/html/posts/frontend/dist;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

As you can see, the dist folder is the root. This is where the build command generates its output. The frontend config needs only one location where we try to load:

This is still not an optimized config but it works pretty fine. We're gonna talk about optimization in a dedicated article.

All of these examples and content come from my new 465-page book "DevOps with Laravel". Check it out:

DevOps with Laravel