Basic nginx + PHP-FPM config for securely hosting multiple websites

I previously wrote a post about setting up PHP-FPM and Apache in a scalable way with vhosts and separate FPM pools. Since then, unfortunately, I feel like not only is the article outdated (written for an old version of CentOS) but that it also doesn’t reflect my current feelings towards scalable web hosting. I wanted to take a post and provide what is, in my opinion, a better way to set up a web hosting server. In this post, I’m going to walk you through setting up the nginx webserver as well as PHP-FPM on the backend. I’ll also explore options for hosting multiple versions of PHP on your server, and finally walk you through setting up a website from scratch, including SSL setup and sample configuration files.

Initial package install

First off, we need to get our hosting server configured. You don’t need a lot for an nginx/FPM hosting configuration – just the nginx and PHP packages. I’ll go through the setup for PHP later, but for nginx, you can install using yum install epel-release && yum install nginx on CentOS, or apt-get install nginx for Debian/Ubuntu.

nginx Server Setup

Since we’ll be using different configuration files for each individual website, there are very few changes to be made to the core nginx configuration files. Mostly, we’ll add some settings to ensure secure serving of HTTPS pages and then I’ll walk through the basic virtual host setup.

Let’s start with the basic config, located in /etc/nginx/nginx.conf. I’ve cut out all the cruft, so all you should need is the following:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
  worker_connections 768;
}

http {

  ##
   # Basic Settings
  ##

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  server_tokens off; # remove stuff like version number from showing up when people visit pages
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  ##
  # Logging Settings
  ##

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

  ##
  # Gzip Settings
  ##

  gzip on;
  gzip_disable "msie6";

  ##
  # Virtual Host Configs
  ##

  include /etc/nginx/conf.d/*.conf; # Just a note - this directory is empty! You can put more stuff here if you want though.
  include /etc/nginx/sites-enabled/*;
}

There’s a lot missing here, right? No PHP setup, no SSL config. This is all actually perfectly normal! What I did is break these out into stuff to go in the /etc/nginx/snippets/ directory, to be included in the individual virtual hosts. So let’s break those down.

ssl-params.conf

Here’s the first one, which is used for SSL settings. I pretty much grabbed these settings from cipherlist and did a little bit of tweaking, but realistically you can kind of take the cipherlist config as it is. I would be hesitant about enabling ssl_session_tickets without testing – if you have any SSL-enabled site that has it set to on, all sites with it set to off will break entirely. You might want to look at enabling TLSv1.1 and some additional cipher suites if you have to support legacy web clients.

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains; preload'; # don't keep includeSubDomains on unless you control and have SSL on all subdomains
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/nginx/snippets/dhparam.pem; # Remember to generate this file - I recommend generating it at 4096-bit strength

php_settings.conf

Next, let’s get PHP settings in place. Like I said, this is a guide for a standard nginx + FPM setup, so we need to make sure these are in place. I feel the following covers most use cases.

fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors on;
fastcgi_ignore_client_abort off;
fastcgi_connect_timeout 60;
fastcgi_read_timeout 120;
fastcgi_send_timeout 120;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;

Sample virtual host

This is a little bit of a two-step process, since you need to get SSL certificates set up (I cannot imagine a situation, in a world where letsencrypt exists, where you would not serve something over plain HTTP.)

So, basic vhost, very straightforward, stick it in /etc/nginx/sites-available and then create a link to /etc/nginx/sites-enabled:

server {
  listen 80;
  server_name domain.com www.domain.com; #add any alternate domains for this site
  root /var/www/html/domain.com;
}

Literally just saying “hey, listen on port 80 for any of these domains, go look in the folder specified”. Super easy. As long as you’ve got this set up, you can just run certbot certonly --webroot -w /var/www/html/domain.com -d domain.com -d www.domain.com, or just use the Certbot nginx plugin, or honestly whatever else you want to do to get that cert in place as long as you’ve started the nginx service. Once you’ve got the cert, though, it’s easy to set everything else you need up:

server {
  listen 443 ssl http2;
  server_name domain.com www.domain.com;
  root /var/www/html/domain.com;
  ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem; # or wherever that lives in your install
  ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.com;
  include /etc/nginx/snippets/ssl-params.conf; # Remember when we set these up earlier?

  location / {
    # Sample nonwww to www (canonical) redirect, if you need it
    if ($http_host ~* "^domain\.com$") {
      rewrite ^/$ https://www.domain.com/? redirect;
      rewrite ^(.*)$ https://www.domain.com$request_uri redirect;
    }
  }
}

server {
  listen 80;
  server_name domain.com www.domain.com;
  return 301 https://$host$request_uri;
}

So looking at this…we’ve gotten the redirect set up to make sure we’re serving HTTPS on the domain, but something is still missing here, right? We haven’t gotten PHP set up yet. Fortunately, that’s pretty straightforward, so let’s go over that next.

PHP-FPM Setup

The easiest way to get started here is to simply install the php-fpm package from the repository of your choice (honestly, installing php-* would even work). This should pull down whatever version of PHP that your distro supports out of the box. Depending on what your application requirements are, though, you might need to get a different version, so let’s cover how you might do that. On Debian or Ubuntu, you’d look at the deb.sury.org repository, while on CentOS you’d want the remi-safe repository (available here) or if you only need one PHP version, check out the IUS repo. Both of these have different methods and locations for actually installing the packages, so I won’t get too in depth there.

There is one tweak that you’ll need to make once you get PHP installed, though – hop into the php.ini file (location will vary based on what PHP package you have installed), and find the setting for cgi.fix_pathinfo. Uncomment it and change the value to 0.

Setting up an FPM Pool

The next step is to set up an FPM pool for each site you have on the server. So, for example, in the pre-configured path for FPM pools (usually /etc/php/[...]/pool.d), go ahead and create a new file, name it domain.com.conf, and do something like the following:

[domain.com]

listen = /var/run/domain.com.sock # where the socket lives on your system
listen.allowed_clients = 127.0.0.1
listen.owner = domain
listen.mode = 660
listen.group = www-data # let nginx talk to the socket
user = domain
group = domain

pm = ondemand # change following values depending on your server load; Google "FPM tuning" for more details
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 200
pm.process_idle_timeout=10s

php_admin_value[open_basedir] = /var/www/html/domain.com:/usr/share/php:/tmp
php_admin_value[session.save_path] = /var/www/html/domain.com/tmp
php_admin_value[upload_tmp_dir] = /var/www/html/domain.com/tmp

One more thing before we can actually start the FPM service: we need to create the user. I do useradd domain -s /sbin/nologin -d /var/www/html/domain.com and that pretty much covers it for me. Once that’s done, you can start the FPM service (service name will vary based on the package you install), and it should come up no problem. Now all that’s left is to point nginx at your new FPM pool, so let’s go back to the virtual host you created.

Adding PHP to nginx sites

Pretty simple set of steps here, really. Make the following changes to your vhost:

  • Add index index.php; inside the HTTPS server block
  • add any framework-specific rules to the end of your location / block (for example, a lot of PHP developed applications require try_files $uri $uri/ /index.php?$args;)
  • add the following block of code after your location / block
location ~ \.php$ {
  try_files $uri =404;
  fastcgi_pass unix:/var/run/domain.com.sock; # match the setting you set in your FPM pool, above
  include fastcgi_params;
  include /etc/nginx/snippets/php_settings.conf;
}

That’s it! You can test your new server configuration with nginx -t and if it comes back as successful, just reload the service and your PHP site should be up and running.

Final Thoughts

Default virtual host

To make sure that nobody can just point random domains to your server and have sites get served, I recommend setting up default virtual hosts as kind of a “null route”. Here’s an example of what you could put in /etc/nginx/sites-available/default.conf:

server {
  listen 443 default_server ssl http2;
  ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
  ssl_certificate /etc/letsencrypt/live/domain.com/privkey.pem;
  include /etc/nginx/snippets/ssl-params.conf;
  root /var/www/html/forbidden;
  return 404;
}
server {
  listen 80 default_server;
  root /var/www/html/forbidden;
  return 404;
}

You do need to use a valid certificate for this – e.g., not self-signed – or you’re going to run into issues with OCSP stapling getting disabled server-wide. If you have a throwaway domain or subdomain you can use, I’d recommend using that.

I hope you found this helpful! I’m going to be building on this post in the future to talk about how you can extend this server configuration to host sites on multiple platforms (Python, node, etc) without impacting your existing websites. In the meantime, please leave a comment below if you have any questions or suggestions.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.