Modern PHP (2015)
Part III. Deployment, Testing, and Tuning
Chapter 7. Provisioning
After you choose a host for your application, it’s time to configure and provision the server for your PHP application. I’ll be honest—provisioning a server is an art, not a science. How you provision your server depends entirely on your application’s needs.
NOTE
If you use a PaaS, your server infrastructure is managed by the PaaS provider. All you have to do is follow the provider’s instructions to move your PHP application onto their platform, and you’re ready to go.
If you don’t use a PaaS, you must provision either a VPS or dedicated server to run your PHP application. Provisioning a server is not as hard as it sounds (stop laughing), but it does require familiarity with the command line. If the command line is alien to you, you’re better off with a PaaS like Engine Yard or Heroku.
I don’t consider myself a system administrator. However, basic system adminstration is an incredibly valuable skill for application developers that enables more flexible and robust application development. In this chapter, I’ll share my system administration knowledge so you can feel comfortable opening a terminal to provision a server for your PHP application. Afterward, I’ll suggest a few additional resources for you to continue improving your system administration skills.
NOTE
In this chapter, I assume you know how to edit a text file using a command-line editor like nano or vim (these are available on most Linux distributions). Otherwise, you’ll need an alternative method of accessing and editing files on your server.
Our Goal
First, we need to acquire a virtual private or dedicated server. Next, we need to install a web server to receive HTTP requests. Finally, we need to set up and manage a group of PHP processes to handle PHP requests; these processes must communicate with our web server.
Several years ago, it was common practice to install the Apache web server and the Apache mod_php module. The Apache web server spawns a unique child process to handle each HTTP request. The Apache mod_php module embeds a unique PHP interpreter inside each spawned child process—even processes that serve only static assets like JavaScript, images, or stylesheets. This is a lot of overhead that wastes system resources. I see fewer and fewer PHP developers use Apache nowadays because there are more efficient solutions.
Today, we use the nginx web server, which sits in front of (and forwards PHP requests to) a collection of PHP-FPM processes. That’s the solution I’ll demonstrate in this chapter.
Server Setup
First, let’s set up a virtual private server (VPS). I absolutely adore Linode. It isn’t the cheapest VPS provider, but it’s one of the most reliable. Head over to Linode’s website (or your preferred vendor) and purchase a new VPS. Your vendor will ask you to choose a Linux distribution and a root password for your new server.
TIP
Many VPS providers, like Linode and Digital Ocean, bill by the hour. This means you can fire up and play with a VPS at virtually zero cost.
First Login
The first thing you should do is log in to your new server. Let’s do that now. Open a terminal on your local machine and ssh into your server. Be sure you swap in your own machine’s IP address:
ssh root@123.456.78.90
You may be asked to confirm the authenticity of your new server. Type yes and press Enter:
The authenticity of host '123.456.78.90 (123.456.78.90)' can't be established.
RSA key fingerprint is 21:eb:37:f3:a5:d3:c0:77:47:c4:15:3d:3c:dc:3c:d1.
Are you sure you want to continue connecting (yes/no)?
Next, you’ll be prompted for the root user’s password. Type the password and press Enter:
root@123.456.78.90's password:
You are now logged into your new server!
Software Updates
The very next thing you should do is update your operating system’s software with these commands.
# Ubuntu
apt-get update;
apt-get upgrade;
# CentOS
yum update
These commands spit out a lot of information as software updates for your operating system are downloaded and applied. This is an important first step because it ensures you have the latest updates and security fixes for your operating system’s default software.
Nonroot User
Your new server is not secure. Here are a few good practices to harden your new server’s security.
Create a nonroot user. You should log in to your server as this nonroot user in the future. The root user has unlimited power on your server. It is God. It can run any command without question. You should make it as difficult as possible to access your server as the root user.
Ubuntu
Create a new nonroot user named deploy with the command in Example 7-1. Enter a user password when prompted, and follow the remaining on-screen instructions.
Example 7-1. Create nonroot user on Ubuntu
adduser deploy
Next, assign the deploy user to the sudo group with this command:
usermod -a -G sudo deploy
This gives the deploy user sudo privileges (i.e., it can perform privileged tasks with password authentication).
CentOS
Create a new nonroot user named deploy with this command:
adduser deploy
Give the deploy user a password with this command. Enter and confirm the new password when prompted:
passwd deploy
Next, assign the deploy user to the wheel group with this command:
usermod -a -G wheel deploy
This gives the deploy user sudo privileges (i.e., it can perform privileged tasks with password authentication).
SSH Key-Pair Authentication
On your local machine, you can log into your new server as the nonroot deploy user like this:
ssh deploy@123.456.78.90
You’ll be prompted for the deploy user’s password, and then you’ll be logged in to the server. We can make the login process more secure by disabling password authentication. Password authentication is vulnerable to brute-force attacks in which bad guys try to guess your password over and over in quick succession. Instead, we’ll use SSH key-pair authentication when we ssh into our server.
Key-pair authentication is a complex subject. In basic terms, you create a pair of “keys” on your local machine. One key is private (this stays on your local machine), and one key is public (this goes on the remote server). They are called a key pair because messages encrypted with the public key can be decrypted only by the related private key.
When you log in to the remote machine using SSH key-pair authentication, the remote machine creates a random message, encrypts it with your public key, and sends it to your local machine. Your local machine decrypts the message with your private key and returns the decrypted message to the remote server. The remote server then validates the decrypted message and grants you access to the server. This is a dramatic simplification, but you get the point.
If you log in to your remote server from many different computers, you probably do not want to use SSH key-pair authentication. This would require you to generate public/private SSH key pairs for each local computer and copy each key pair’s public key to your remote server. In this case, it’s probably preferable to continue using password authentication with a secure password. However, if you are only accessing your remote server from a single local computer (as many developers often do), SSH key-pair authentication is the way to go. You can create an SSH key-pair on your local machine with this command:
ssh-keygen
Follow the subsequent on-screen instructions and enter the requested information when prompted. This command creates two files on your local machine: ~/.ssh/id_rsa.pub (your public key) and ~/.ssh/id_rsa (your private key). The private key should stay on your local computer and remain a secret. Your public key, however, must be copied onto your new server. We can copy the public key with the scp (secure copy) command:
scp ~/.ssh/id_rsa.pub deploy@123.456.78.90:
Be sure you include the trailing : character! This command uploads your public key to the deploy user’s home directory on your remote server. Next, log in to your remote server as the deploy user. After you log in to your remote server, make sure the ~/.ssh directory exists. If it does not exist, create the ~/.ssh directory with this command:
mkdir ~/.ssh
Next, create the ~/.ssh/authorized_keys file with this command:
touch ~/.ssh/authorized_keys
This file will contain a list of public keys that are allowed to log into this remote server. Execute this command to append your recently uploaded public key to the ~/.ssh/authorized_keys file:
cat ~/id_rsa.pub >> ~/.ssh/authorized_keys
Finally, we need to modify a few directory and file permissions so that only the deploy user can access its own ~/.ssh directory and read its own ~/.ssh/authorized_keys file. Assign these permissions with these commands:
chown -R deploy:deploy ~/.ssh;
chmod 700 ~/.ssh;
chmod 600 ~/.ssh/authorized_keys;
We’re done! On your local machine, you should now be able to ssh into the remote server without entering a password.
NOTE
You can only ssh into your remote server without a password from the local machine that has your private key!
Disable Passwords and Root Login
Let’s make the remote server even more secure. We’ll disable password authentication for all users, and we’ll prevent the root user from logging in—period. Remember, the root user can do anything, so we want to make it as difficult as possible to access our server as the root user.
Log in to the remote server as the deploy user and open the /etc/ssh/sshd_config file in your preferred text editor. The is the SSH server software’s configuration file. Find the PasswordAuthentication setting and change its value to no; uncomment this setting if necessary. Find thePermitRootLogin setting and change its value to no; uncomment this setting if necessary. Save your changes and restart the SSH server with this command to apply your changes:
# Ubuntu
sudo service ssh restart
# CentOS
sudo systemctl restart sshd.service
You’re done. You’ve secured your server, and it’s time to install additional software to run your PHP application. From this point forward, all instructions should be completed on the remote server as the nonroot deploy user.
NOTE
Server security is an ongoing task that should be constantly monitored. I recommend you implement a firewall in addition to my previous instructions. Ubuntu users can use UFW. CentOS users can use iptables.
PHP-FPM
PHP-FPM (PHP FastCGI Process Manager) is software that manages a pool of related PHP processes that receive and handle requests from a web server like nginx. The PHP-FPM software creates one master process (usually run by the operating system’s root user) that controls how and when HTTP requests are forwarded to one or more child processes. The PHP-FPM master process also controls when child PHP processes are created (to answer additional web application traffic) and destroyed (if they are too old or no longer necessary). Each PHP-FPM pool process lives longer than a single HTTP request, and it can handle 10, 50, 100, 500, or more HTTP requests.
Install
The simplest way to install PHP-FPM is with your operating sytem’s native package manager, as demonstrated by the following commands.
TIP
See Appendix A for a detailed PHP-FPM installation guide.
# Ubuntu
sudo apt-get install python-software-properties;
sudo add-apt-repository ppa:ondrej/php5-5.6;
sudo apt-get update;
sudo apt-get install php5-fpm php5-cli php5-curl \
php5-gd php5-json php5-mcrypt php5-mysqlnd;
# CentOS
sudo rpm -Uvh \
http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-5.noarch.rpm;
sudo rpm -Uvh \
http://rpms.famillecollet.com/enterprise/remi-release-7.rpm;
sudo yum -y --enablerepo=epel,remi,remi-php56 install php-fpm php-cli php-gd \
php-mbstring php-mcrypt php-mysqlnd php-opcache php-pdo php-devel;
TIP
If the EPEL rpm installation fails, open a web browser and navigate to http://dl.fedoraproject.org/pub/epel/7/x86_64/e/. Look for an updated EPEL release version and use that.
Global Configuration
On Ubuntu, the primary PHP-FPM cofiguration file is /etc/php5/fpm/php-fpm.conf. On CentOS, the primary PHP-FPM configuration file is /etc/php-fpm.conf. Open this file in your preferred text editor.
NOTE
PHP-FPM configuration files use the INI file format. Learn more about the INI format on Wikipedia.
These are the most important global PHP-FPM settings that I recommend you change from their default values. These two settings might be commented out by default; uncomment them if necessary. These settings prompt the master PHP-FPM process to restart if a specific number of its child processes fail within a specific interval of time. These settings are a basic safety net for your PHP-FPM processes that can resolve simple issues. They are not a solution to more fundamental problems caused by bad PHP code.
emergency_restart_threshold = 10
The maximum number of PHP-FPM child processes that can fail within a given time interval until the master PHP-FPM process gracefully restarts
emergency_restart_interval = 1m
The length of time that governs the emergency_restart_threshold setting
NOTE
Read more about PHP-FPM global configuration at http://php.net/manual/en/install.fpm.configuration.php.
Pool Configuration
Elsewhere in the PHP-FPM configuration file is a section named Pool Definitions. This section contains configuration settings for each PHP-FPM pool. A PHP-FPM pool is a collection of related PHP child processes. One PHP application typically has its own PHP-FPM pool.
On Ubuntu, the Pool Definitions section contains this one line:
include=/etc/php5/fpm/pool.d/*.conf
CentOS includes the pool definition files at the top of the primary PHP-FPM configuration file with this line:
include=/etc/php-fpm.d/*.conf
This line prompts PHP-FPM to load individual pool definition files located in the /etc/php5/fpm/pool.d/ directory (for Ubuntu) or the /etc/php-fpm.d/ directory (for CentOS). Navigate into this directory, and you should see one file named www.conf. This is the configuration file for the default PHP-FPM pool named www. Open this file in your preferred text editor.
NOTE
Each PHP-FPM pool configuration begins with a [ character, the pool name, and a ] character. The default PHP-FPM pool configuration, for example, begins with [www].
Each PHP-FPM pool runs as the operating system user and group that you specify. I prefer to run each PHP-FPM pool as a unique nonroot user to help me identify each PHP application’s PHP-FPM processes on the command line with the top or ps aux commands. This is a good habit, too, because each PHP-FPM pool’s processes are inherently sandboxed by the permissions available to their operating system user and group.
We’ll configure the default www PHP-FPM pool to run as the deploy user and group. If you haven’t already, open the www PHP-FPM pool configuration file in your preferred text editor. Here are the settings I recommend you change from their default values:
user = deploy
The system user that owns this PHP-FPM pool’s child processes. Set this to your PHP application’s nonroot operating system user name.
group = deploy
The system group that owns this PHP-FPM pool’s child processes. Set this to your PHP application’s nonroot operating system group name.
listen = 127.0.0.1:9000
The IP address and port number on which this PHP-FPM pool listens for and accepts inbound requests from nginx. The value 127.0.0.1:9000 instructs this specific PHP-FPM pool to listen for incoming connections on local port 9000. I use port 9000, but you can use any nonprivileged port number (any port number greater than 1024) that is not already in use by another system process. We’ll revisit this setting when we configure our nginx virtual host.
listen.allowed_clients = 127.0.0.1
The IP address(es) that can send requests to this PHP-FPM pool. For security reasons, I set this to 127.0.0.1. This means that only the current machine can forward requests to this PHP-FPM pool. This setting might be commented out by default. Uncomment this setting if necessary.
pm.max_children = 51
This value sets the total number of PHP-FPM pool processes that can exist at any given time. There is no correct value for this setting. You should test your PHP application, determine how much memory each individual PHP process uses, and set this to the total number of PHP processes that your machine’s available memory can accommodate. Most small to medium-sized PHP applications often use between 5 MB and 15 MB of memory for each individual PHP process (your mileage may vary). Assuming we are on a machine with 512 MB of memory available to this PHP-FPM pool, we can set this value to 512MB total / 10MB per process, or 51 processes.
pm.start_servers = 3
The number of PHP-FPM pool processes that are available immediately when PHP-FPM starts. Again, there is no correct value for this setting. For most small or medium-sized PHP applications, I recommend a value of 2 or 3. This ensures that your PHP application’s initial HTTP requests don’t have to wait for PHP-FPM to initialize PHP-FPM pool processes. Two or three processes are already ready and waiting.
pm.min_spare_servers = 2
The smallest number of PHP-FPM pool processes that exist when your PHP application is idle. This will typically be in the same ballpark as your pm.start_servers setting, and it ensures that new HTTP requests don’t have to wait for PHP-FPM to initialize new pool processes to handle new requests.
pm.max_spare_servers = 4
The largest number of PHP-FPM pool processes that exist when your PHP application is idle. This will typically be a bit more than your pm.start_servers setting, and it ensures that new HTTP requests don’t have to wait for PHP-FPM to initialize new pool processes to handle new requests.
pm.max_requests = 1000
The maximum number of HTTP requests that each PHP-FPM pool process handles before being recycled. This setting helps us avoid accumulating memory leaks caused by poorly coded PHP extensions or libraries. I recommend a value of 1000, but you should tweak this based on your own application’s needs.
slowlog = /path/to/slowlog.log
The absolute filesystem path to a log file that records information about HTTP requests that take longer than {n} number of seconds to process. This is helpful for identifying and debugging bottlenecks in your PHP applications. Bear in mind, this PHP-FPM pool’s user or group must have permission to write to this file. The value /path/to/slowlog.log is an example; replace this value with your own file path.
request_slowlog_timeout = 5s
The length of time after which the current HTTP request’s backtrace is dumped to the log file specified by the slowlog setting. The value you choose depends on what you consider to be a slow request. A value of 5s is a reasonable value to start with.
After you edit and save the PHP-FPM configuration file, restart the PHP-FPM master process with this command:
# Ubuntu
sudo service php5-fpm restart
# CentOS
sudo systemctl restart php-fpm.service
NOTE
Read more about PHP-FPM pool configuration at http://php.net/manual/install.fpm.configuration.php.
nginx
nginx (pronounced in gen ex) is a web server similar to Apache, but it’s much simpler to configure and often uses less system memory. I don’t have time to dig into nginx in detail, but I do want to show you how to install nginx on your server and forward appropriate requests to your PHP-FPM pool.
Install
The simplest way to install nginx is with your operating system’s native package manager.
Ubuntu
On Ubuntu, install nginx with a PPA. This is an Ubuntu-specific term for a prepackaged archive maintained by the nginx community:
sudo add-apt-repository ppa:nginx/stable;
sudo apt-get update;
sudo apt-get install nginx;
CentOS
On CentOS, install nginx using the same EPEL third-party software repository we added earlier. The default CentOS software repositories might not have the latest nginx version:
sudo yum install nginx;
sudo systemctl enable nginx.service;
sudo systemctl start nginx.service;
Virtual Host
Next, we’ll configure an nginx virtual host for our PHP application. A virtual host is a group of settings that tell nginx our application’s domain name, where the PHP application lives on the filesystem, and how to forward HTTP requests to the PHP-FPM pool.
First, we must decide where our application lives on the filesystem. The PHP application files must live in a filesystem directory that is readable and writable by the nonroot deploy user. For this example, I’ll place application files in the /home/deploy/apps/example.com/current directory. We’ll also need a directory to store application log files. I’ll place log files in the /home/deploy/apps/logs directory. Use these commands to create the directories and assign correct permissions:
mkdir -p /home/deploy/apps/example.com/current/public;
mkdir -p /home/deploy/apps/logs;
chmod -R +rx /home/deploy;
Place your PHP application in the /home/deploy/apps/example.com/current directory. The nginx virtual host configuration assumes your PHP application has a public/ directory; this is the virtual host document root.
Each nginx virtual host has its own configuration file. If you use Ubuntu, create the /etc/nginx/sites-available/example.conf configuration file. If you use CentOS, create the /etc/nginx/conf.d/example.conf configuration file. Open the example.conf configuration file in your preferred text editor.
nginx virtual host settings live inside a server {} block. Here is the complete virtual host configuration file:
server {
listen 80;
server_name example.com;
index index.php;
client_max_body_size 50M;
error_log /home/deploy/apps/logs/example.error.log;
access_log /home/deploy/apps/logs/example.access.log;
root /home/deploy/apps/example.com/current/public;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
}
}
Copy and paste this code into the example.conf virtual host configuration file. Make sure you update the server_name setting and swap the error_log, access_log, and root paths with appropriate values. Here’s a quick explanation of each virtual host setting:
listen
The port number on which nginx listens for inbound HTTP requests. In most cases, this is port 80 for HTTP traffic or port 443 for HTTPS traffic.
server_name
The domain name that identifies this virtual host. Change this to your application’s domain name, and ensure the domain name points at your server’s IP address. nginx sends an HTTP request to this virtual host if the request’s Host: header matches the virtual host’s server_namevalue.
index
The default files served if none is specified in the HTTP request URI.
client_max_body_size
The maximum HTTP request body size accepted by nginx for this virtual host. If the request body size exceeds this value, nginx returns a HTTP 4xx response.
error_log
The filesystem path to this virtual host’s error log file.
access_log
The filesystem path to this virtual host’s access log file.
root
The document root directory.
There are also two location blocks. These tell nginx how to handle HTTP requests that match specific URL patterns. The first location / {} block uses a try_files directive that looks for real files that match the request URI. If a file is not found, it looks for a directory that matches the request URI. If a directory is not found, it rewrites the HTTP request URI to /index.php and appends the query string if available. The rewritten URL, or any request whose URI ends with .php, is managed by the location ~ \.php {} block.
The location ~ \.php {} block forwards HTTP requests to our PHP-FPM pool. Remember how we set up our PHP-FPM pool to listen for requests on port 9000? This block forwards PHP requests to port 9000, and the PHP-FPM pool takes over.
NOTE
There are a few extra lines in the location ~ \.php {} block. These lines prevent potential remote code execution attacks.
On Ubuntu, we must symlink the virtual host configuration file into the /etc/nginx/sites-enabled/ directory with this command:
sudo ln -s /etc/nginx/sites-available/example.conf \
/etc/nginx/sites-enabled/example.conf;
Finally, restart nginx with this command:
# Ubuntu
sudo service nginx restart
# CentOS
sudo systemctl restart nginx.service
Your PHP application is up and running! There are many ways to configure nginx. I’ve included only the most essential nginx settings in this chapter because this is a PHP book, not an nginx book. You can learn more about nginx configuration at any of these helpful resources:
§ http://nginx.org/
§ https://github.com/h5bp/server-configs-nginx
§ https://serversforhackers.com/editions/2014/03/25/nginx/
Automate Server Provisioning
Server provisioning is a lengthy process. It’s also not a fun process, especially if you manually provision many servers. Fortunately, there are tools available that help automate server provisioning. Some popular server provisioning tools are:
§ Puppet
§ Chef
§ Ansible
§ SaltStack
Each tool is different, but they all accomplish the same goal—they automatically provision new servers based on your exact specifications. If you are responsible for multiple servers, I strongly encourage you to explore provisioning tools, because they save a ton of time.
Delegate Server Provisioning
There are online services, too, that perform server provisioning on your behalf. An example service is Forge by Taylor Otwell. I was a Forge beta tester, and it really is a helpful service. Forge can provision multiple servers on Linode, Digital Ocean, and other popular VPS providers.
Each server provisioned by Forge is automatically secured using the same security practices I demonstrated earlier. Forge automatically installs an nginx and PHP-FPM software stack. Forge also simplifies PHP application deployment, SSL certificate installation, CRON task creation, and other mundane or confusing system administration tasks. I highly recommend Forge if system administration isn’t your cup of tea.
Further Reading
I find system administration fascinating. I don’t want to do it as a full-time job, but I enjoy tinkering on the command line. The best system administration learning resource for developers, in my opinion, is Servers for Hackers by Chris Fidao.
What’s Next
In this chapter we discussed how to provision a server to run PHP applications. Next we’ll talk about how to tune your server to eke out maximum performance for your PHP application.