A Comprehensive Guide to setting up LEMP Stack (Linux, Nginx, MariaDB, and PHP) in Ubuntu 20.04 LTS.
There's a lot of guides for setting up the LAMP/LEMP stack on the web, and most of them are scattered and sometimes only provide the default config without any optimization. Here I am going to tell you how to install and configure the software that is production-ready and optimized for daily use.
Let's start with what you need.
- A fairly sized VM, or bare-metal installation for good measure.
- Freshly installed Ubuntu OS and please, please use LTS releases for your production servers.
- Experience in working with command line stuff.
- Patience.
In this tutorial, I am going to use a standard cloud VM from Vultr with these specs:
2vCPU, 4GB RAM and 80GB SSD
You could try and set up your Vultr account by clicking here.
Setup the required repositories.
# Enable secure APT downloads
sudo apt install software-properties-common dirmngr apt-transport-https
# For nginx
sudo add-apt-repository ppa:ondrej/nginx-mainline
# For PHP
sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
# For MariaDB
sudo apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc'
sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el] https://mirror.djvg.sg/mariadb/repo/10.6/ubuntu focal main'
As you notice above, I'm using the excellent PPA provided by Ondřej Surý.
The build of Nginx has already been compiled for TLS 1.3 and HTTP/2 support, and his PHP repositories are constantly up-to-date with the latest version and complete extensions support.
For the MariaDB installation, I am using a local official mirror provided by MariaDB based in Singapore, if you want to change to your local mirror you could check here and replace the URL.
Update your system.
After setting up the repositories, you should update your system by using the standard:
sudo apt update && sudo apt -y upgrade
Setup all the software.
sudo apt install nginx curl git zip unzip wget mariadb-server mariadb-client haveged
The script above will install a few things.
1. Nginx - the webserver we going to use.
2. curl, git, zip, unzip, wget - This batch of software is used for many PHP extension dependencies.
3. MariaDB - the database we going to use.
4. Haveged - an entropy daemon to help cryptographic randomness especially when running in a low-entropy scenario like a virtual machine.
Next, we are going to set up PHP and choose your flavor 7.4 or 8.0:
# For PHP 7.4
sudo apt install php7.4-common php7.4-cli php7.4-curl php7.4-fpm php7.4-gd php7.4-gmp php7.4-intl php7.4-json php7.4-mbstring php7.4-mysql php7.4-opcache php7.4-readline php7.4-xml php7.4-zip
# For PHP8
sudo apt install php8.0-common php8.0-cli php8.0-curl php8.0-fpm php8.0-gd php8.0-gmp php8.0-intl php8.0-json php8.0-mbstring php8.0-mysql php8.0-opcache php8.0-readline php8.0-xml php8.0-zip
You could add or remove the extension as you choose, the list I provided above is what I encounter daily on setting up applications for clients and such.
Follow this link to check for the available extensions package.
Initial MariaDB setup.
When we finished installing the packages from the previous step, the best practice is always to run the included security tweaks script.
Execute the script:
sudo mysql_secure_installation
Then, follow these steps:
- Ignore the "Enter current password for root" by pressing enter, we won't use the root account as by default root uses socket authentication which for most PHP applications only allows database connection by using password auth.
- On the "Set root password?" question just type N and enter.
- From there, just type Y on the rest of the question to securely implement the best-practice changes.
Next, we are going to create a new administrative account for use with the application.
sudo mariadb
CREATE USER 'administrator'@'localhost' IDENTIFIED BY 'password';
GRANT ALL ON *.* TO 'administrator'@'localhost' WITH GRANT OPTION;
Be sure to change the username and password above, next flush the session privileges.
FLUSH PRIVILEGES;
After you finish that, you could type exit to return to the terminal and we could try the new account by typing:
mysql -u administrator
You will be prompted to enter your password, and if you're successfully login then congrats, now MariaDB is installed and configured.
Configuring Kernel Variables.
It may sound scary, but if you want to achieve high web concurrency you should try to tune the kernel variable at /etc/sysctl.conf
These scripts are based on a few references (which you should read if you are a performance freak!):
- https://www.nginx.com/blog/tuning-nginx/
- https://www.brendangregg.com/blog/2015-03-03/performance-tuning-linux-instances-on-ec2.html
- https://russ.garrett.co.uk/2009/01/01/linux-kernel-tuning/
- https://fasterdata.es.net/host-tuning/linux/
After changing the value in /etc/sysctl.conf, you could enable it by using the following commands:
sudo sysctl -p
Configure file-max limit for Nginx.
By default, the ulimit value or process file-max value for Nginx is 1024.
We could change that by modifying /etc/security/limits.conf file and add this line.
* soft nofile 65535
* hard nofile 65535
Update /etc/default/nginx and add this value
ULIMIT="-n 65535"
On the first installation, the Nginx service wasn't enabled automatically at startup, you could enable the service by executing the command below.
sudo systemctl enable nginx
sudo systemctl start nginx
Update /etc/systemd/system/nginx.service or /lib/systemd/system/nginx.service by adding LimitNOFILE=65535 under the [Service] block, then reload the systemd by executing this command
systemctl daemon-reload
sudo service nginx restart
After changing the value, you could validate the config by running this command
cat /proc/`ps -aux | grep -m 1 nginx | awk -F ' ' '{print $2}'`/limits | grep "open files" | awk -F ' ' '{print $4}'
# The output should be: 65535
Tuning Nginx
Next, let's configure the /etc/nginx/nginx.conf file to start optimizing the default settings based on the configuration below.
if you compare the default config with this, it adds a few things.
- The worker_processes value is set from auto to 2, this value should correspond with how many CPU cores you have as a common practice.
- worker_rlimit_nofile value corresponding to the open file-max configuration previously.
- worker_connections value changed from 768 to 4096, you should know that the maximum concurrent connection value is determined like this:
(Max clients = worker_connections * worker_processes)
For my baseline configuration, it left me with an 8k limit that I believe is a good start to check if we should increase or decrease it depending on the system load and config. - multi_accept essentially allows each process to accept multiple connections as much as possible, this config relates to the worker_connections we set.
- use epoll is a newer, more optimized connection method with the newer Linux version.
It's essential to switch to it. - tcp_nodelay don't buffer data-sends, if you are sending a frequent small burst of data this setting is good to enable.
- The timeout setting is decreased from the default value so it going to terminate a long-standing request in case the application hangs, or issue with the scripts.
Keep in mind, that this value should correspond with how long is the limit for your scripts and application to run, don't take it as face value. - Added buffer optimization setting to override the limitation, also helps to prevent DDOS. But keep in mind the client_max_body_size also determines what your upload file size limit is.
- Enabling the gzip compression for static resources, so you don't need to send the whole file size whenever serving the content.
Setup Nginx Virtual Hosts
After you finish setting up the Nginx configuration, next we're going to set up your website virtual hosts settings. By default, Nginx has one server block configured to serve a directory at /var/www/html by creating new virtual host settings, you could easily manage and host multiple sites on a single host.
First, create a root web directory and set user permission for your_domain as follows:
sudo mkdir /var/www/your_domain/
sudo mkdir /var/www/your_domain/htdocs/
sudo mkdir /var/www/your_domain/logs
sudo chown -R $USER:$USER /var/www/your_domain
Next, create a new file in /etc/nginx/sites-available and you could name it with the domain name like /etc/nginx/sites-available/your_domain and include it with the config below.
Enable your new configuration.
sudo ln -sf /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/your_domain
sudo service nginx restart
Configure PHP-FPM.
After you finish configuring Nginx, next we are going to configure PHP-FPM.
Open php-fpm.conf in /etc/php/7.4/fpm/php-fpm.conf or change the 7.4 to 8.0 if you are using PHP 8 and change the following variables to this value:
These settings tell php-fpm if the child processes fail within a minute, the main process forces them to restart. It's useful when handling memory leaks within a process.
For the /etc/php/7.4/fpm/pool.d/www.conf:
listen = 127.0.0.1:9000
pm = static
pm.max_children = 16
pm.max_requests = 2000
I found out that using TCP sockets, rather than Unix ones like the default configuration for the listener allows more flexibility and solves compatibility issues if I want to use something like a custom SERVER_NAME value or use the built-in fpm status monitoring.
You also may notice I am changing from dynamic process management to static and for that, I refer you to read more about using pm static from the wonderful article by Hayden James.
https://haydenjames.io/php-fpm-tuning-using-pm-static-max-performance/
Configure php.ini.
You could safely skip this part if you think the default configuration is enough, but there are a few things that you should know when setting up your application.
The default path for the file is /etc/php/7.4/fpm/php.ini
- display_errors
This directive allows you to control whenever you want to show errors displayed on the screen during the script execution, for production use this value should be turned off to prevent showing unexpected vulnerabilities. - error_reporting
This directive allows you to set the error reporting level, accepting a constant range of E_ALL, E_NOTICE, E_STRICT, E_DEPRECATED.
For example, if you want to set it to E_ALL to show all types of errors. - file_uploads
As the name says this directive allows you to enable/disable HTTP file uploads, if your site doesn't need file upload functionality you are safe to disable this. - upload_max_filesize
If you enable file upload, this directive allows you to increase or decrease the size of file uploads, the default value is 2MB. - post_max_size
This setting allows you to control the maximum size of a POST request and if you enable file uploads this directive needs to be higher than the upload_max_filesize value. - memory_limit
This allows you to set a maximum limit of memory that your script allows to allocate and use, you may want to fine-tune this depending on what your application needs because if you set it too high, poorly written or buggy scripts could consume all the memory your server has. - max_execution_time
This directive allows you to set a maximum amount of time scripts are allowed to run, the default is 30 seconds, similar to the memory_limit please tune and set this value as needed to avoid issues on buggy script. - max_input_time
This directive allows you to set the maximum amount of time a script is allowed to parse incoming form data from a GET or POST.
Validate Installation
After thoroughly following all the installation steps, now let's test and validate our installation to make sure everything works okay.
- Create a PHP info test file by creating one in /var/www/your_domain/htdocs/info.php
with the following content:
<?php
phpinfo();
- Access your test file by visiting your public website domain or IP address
http://server_domain_or_ip/info.php - Your site display output should be like this:
- If the output looks the same, then Nginx and PHP are successfully installed.
Now remove the info.php page to prevent unauthorized disclosure.
Conclusion
With that last step, now you have the knowledge of how to set up a LEMP stack with a good baseline configuration for production usage. There is always a good next step after installing like configuring SSL with Let's Encrypt or uploading your own SSL certificate, configuring a PHP opcache, tweaking MariaDB configuration, and more.
But that's for the next time.
For now, pat yourself on the back and treat yourself to some coffee or tea!