Introduction
Greetings readers! As part of the current series of articles, I talk about how to set up a server for simple projects. This means a server for several sites, with a small load under the most popular CMS such as Bitrix. The main purpose of the article is to point out the mistakes made by junior specialists when performing such a setup. Also point out some things that will make troubleshooting easy and convenient.
It’s not really a LEMP stack as it uses Apache2, but you can use PHP-FPM instead if the developer doesn’t mind implementing such a solution.
In the comments to articles, I often see messages that Apache2 is no longer relevant and other software can be upgraded instead. From myself I can say that until now a large number of small and medium-sized organizations that stand up for maintenance use Apache2 and .htaccess files, so I do not agree with this statement. But then again, if you are an experienced administrator who understands how this bundle works, you can skip this article and bring up what your heart desires.
The article was not written with the aim of taking and mindlessly copying all the commands and getting a ready-made server to host the site. Also, if your particular company uses a different stack, I’m very happy for you, but this does not negate the fact that the software described in this article is still popular, used and administered without any problems.
The previous parts of the articles are available at the following links:
- https://systemadminspro.com/setting-up-a-lemp-server-for-simple-projects-instructions-for-the-little-ones-part-one/ – first part
- https://systemadminspro.com/setting-up-a-lemp-server-for-simple-projects-instructions-for-the-little-ones-part-two/ – second part
I recommend that you read this article before reading.
Well, let’s get started.
In Parts 1 and 2 of the article series, we did the following:
- Installed base packages
- Set up git autocommit to commit changes to system configurations
- Performed basic configuration of exim4, ssh, ftpd
- Gave the appropriate rights to the administrator to administer the server
- Performed basic configuration of Apache2, Nginx, MySQL.
The current article will cover:
- Installing and configuring PHP
- Create virtual hosts for sites
- Setting up mail sending
- Giving developers access to the platform
Installing and configuring PHP
Let’s start installing PHP. Our site will be based on PHP 7.4 , however you can deploy any version you require.
Install the packages needed to install the gpg key:
apt -y install lsb-release apt-transport-https ca-certificates
Download the sury repository key and add it to apt :
wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/php.list
You can proceed to install PHP and its core modules:
apt-get update && apt-get install php7.4 php7.4-cli php7.4-common php7.4-curl php7.4-gd php7.4-geoip php7.4-imagick php7.4-imap php7.4-intl php7.4-mcrypt php7.4-mysql php7.4-apc
After completing all the steps, check the PHP version:
php -v PHP 7.4.27 (cli) (built: Dec 20 2021 21:32:33) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.27, Copyright (c), by Zend Technologies
We also check the currently installed PHP modules:
php -m [PHP Modules] apc apcu calendar Core ctype curl date exif FFI fileinfo filter ftp gd geoip gettext hash iconv imagick imap intl json libxml mcrypt mysqli mysqlnd openssl pcntl pcre PDO pdo_mysql Phar posix readline Reflection session shmop sockets sodium SPL standard sysvmsg sysvsem sysvshm tokenizer Zend OPcache zlib [Zend Modules] Zend OPcache
You can always install additional modules for your project, here we are talking only about basic modules.
To view the available PHP modules that can be installed via apt , use the command:
apt-cache search 'php7.4'
If the modules you need are not in the sury repository , you can install them via pecl.
Let’s start setting the basic PHP parameters. We have two environments cli and apache2 (in our case), each environment has its own php.ini file:
/etc/php/7.4/apache2/php.ini /etc/php/7.4/cli/php.ini
The php.ini file located in apache2 is responsible for the PHP settings used when passing scripts to Apache2 for processing , in our case these will be the settings when clients use the site on the site.
The php.ini file located in the cli is used when executing scripts from the console and cron tasks when calling scripts through the php handler.
We will be making changes to both files. The standard changes for basic PHP work are as follows:
-short_open_tag = Off +short_open_tag = On -post_max_size = 8M +post_max_size = 128M -upload_max_filesize = 2M +upload_max_filesize = 64M -;date.timezone = +date.timezone = Europe/Moscow -session.gc_probability = 0 +session.gc_probability = 10 -session.gc_divisor = 1000 +session.gc_divisor = 100 -session.gc_maxlifetime = 1440 +session.gc_maxlifetime = 14400
I will not dwell on these settings in detail, since they are all described in sufficient detail in the PHP manual.
For PHP to work with the minimum possible permissions in the system, you will need to assign the following rights to directories:
chmod -R o-rwx /etc/php/7.4 chmod 751 /etc/php/7.4 chmod -R o+rX /etc/php/7.4/cli chmod -R o+rX /etc/php/7.4/apache2 chmod -R o+rX /etc/php/7.4/mods-available
At this point, the installation and basic configuration of PHP can be considered complete.
All the minimum necessary components for the site to work have been installed, now we can start creating the site.
Create virtual hosts
In our configuration, each server site will work from its own system user. With this setting, the sites will be isolated from each other, respectively, when accessing the site code if any vulnerability is detected, an attacker will not be able to access the rest of the server sites, as if all sites were working from one user.
Let’s create a system user:
groupadd -g 10002 DOMAIN_NAME useradd -g 10002 -u 10002 -s /bin/bash -d /var/www/DOMAIN_NAME DOMAIN_NAME
Where DOMAIN_NAME is the site name, for example site.ru .
Next, let’s create a site directory, according to the standards, all sites will be located in /var/www :
mkdir -p /var/www/DOMAIN_NAME
After that, let’s go to the site directory and create a directory for the site files:
cd /var/www/DOMAIN_NAME mkdir data
Next, we will also create a directory for temporary files, a directory for storing logs, and a directory for storing session files:
mkdir log sess tmp upload log/apache2 log/nginx
After we assign the site the rights and the owner. In our case, file permissions are standard 644 , directory permissions are 755 , while the permissions on the main directory /var/www/DOMAIN_NAME will be 751 , to limit users who are not owners and are not part of the site group:
chown -R DOMAIN_NAME: /var/www/DOMAIN_NAME chmod 751 /var/www/DOMAIN_NAME chmod -R o-rwx /var/www/DOMAIN_NAME/* chmod o+x data log log/nginx
After all the necessary directories have been created, you will need to create Nginx and Apache2 virtual hosts , let’s start with the second one.
Create an Apache2 virtual host file . Create a virtual host file:
touch /etc/apache2/sites-available/DOMAIN_NAME.conf
In our case, its content will look like this:
ServerAdmin webmaster@DOMAIN_NAME DocumentRoot /var/www/DOMAIN_NAME/data ServerName DOMAIN_NAME ServerAlias www.DOMAIN_NAME AssignUserId DOMAIN_NAME DOMAIN_NAME php_admin_value session.save_path "/var/www/DOMAIN_NAME/sess" php_admin_value upload_tmp_dir "/var/www/DOMAIN_NAME/upload" php_admin_value open_basedir "/var/www/DOMAIN_NAME:." CustomLog /var/www/DOMAIN_NAME/log/apache2/access.log combined ErrorLog /var/www/DOMAIN_NAME/log/apache2/error.log LogLevel error <Directory "/var/www/DOMAIN_NAME/data"> AllowOverride All Options FollowSymLinks Order allow,deny Allow from all
The AssignUserId parameter is responsible for the operation of the web server from the user , in which we specify the name and group of the site user. Since the username and site name are identical, in all cases it will be the name of the site.
Here we set the directory of session files, temporary files and the base directory that restricts web server calls for a specific site:
php_admin_value session.save_path "/var/www/DOMAIN_NAME/sess" php_admin_value upload_tmp_dir "/var/www/DOMAIN_NAME/upload" php_admin_value open_basedir "/var/www/DOMAIN_NAME:."
I would like to note right away that PHP parameters can be set in three places, since this question causes a lot of problems at an interview for a junior vacancy :
- in the first case, the PHP parameters are set in the site’s .htaccess files; the PHP parameters set here will only be valid for the directory in which such a file is located. Parameters have normal precedence.
- in the second case, you can set the PHP parameters inside the php.ini file for the appropriate environment, as we did above. These options will also have normal precedence. In this case, if scripts are accessed, then the parameters specified in .htaccess will have a higher priority than the parameters specified in php.ini
- in the third case, you can set parameters at the Apahce2 virtual host level , such parameters will only apply to the php environment for Apache2, respectively , that is, they will only be applied to http / https requests. At the same time, if you set parameters through the php_value function , then such parameters will be equal in priority to the parameters set in php.ini , but they will be higher relative to them. If the PHP parameter set inside the virtual host is set via php_admin_value, then it will have the highest priority for the apahche2 environment. That is, the parameter set in php_admin_valuewill ignore all identical parameters set elsewhere ( htaccess or php.ini ) Using php_admin_value allows you to set specific settings for a particular site. Most often, directories are set through this function or, for example, the function of sending mail, for a specific site.
After creating the Apache2 virtual host , it must be applied using the command:
a2ensite DOMAIN_NAME
This command will create a symbolic link from the /sites-available directory to the /sites-enabled directory
After that, be sure to test the Aapche2 syntax for errors and re-read the web server configuration files:
Apache2ctl -t Service apache2 reload
After creating a virtual host for Apache2 , let’s start creating a virtual host for Nginx :
Create a virtual host file:
touch /etc/nginx/sites-available/DOMAIN_NAME
In the basic configuration, such a file will have the following content:
server { listen 80; server_name DOMAIN_NAME www.DOMAIN_NAME; access_log /var/www/DOMAIN_NAME/log/nginx/access.log nixys; error_log /var/www/DOMAIN_NAME/log/nginx/error.log; location ~ /\.(svn|git|hg) { deny all; } location ~* ^.+\.(css|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js|swf)$ { root /var/www/DOMAIN_NAME/data; expires max; access_log off; } location / { proxy_pass http://127.0.0.1:81; # apache proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 10m; client_body_buffer_size 1280k; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; proxy_buffer_size 4k; proxy_buffers 4 32k; proxy_busy_buffers_size 64k; proxy_temp_file_write_size 64k; } }
View location :
location ~* ^.+\.(css|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js|swf)$ { root /var/www/DOMAIN_NAME/data; expires max; access_log off; }
Responsible for handling static at the Nginx level . The location lists the file formats for processing, if there are statics of other formats among the files of the site, you just need to add the format to the location and re-read the web server configuration.
In the previous article, I was asked how the processing of statics at the Nginx level and the transfer of PHP requests to Apache2 will be separated , due to this location , such requests are transferred.
All other requests will be proxied to Apache2 , namely to server port 81 :
proxy_pass http://127.0.0.1:81; # apache
This completes the Nginx virtual host setup . Let’s create a virtual host symbolic link in sites-enabled :
ln -s /etc/nginx/sites-available/DOMAIN_NAME /etc/nginx/sites-enabled/DOMAIN_NAME
Let’s test the Nginx configuration and reread it:
nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
service nginx reload
After that, be sure to check that the web server is running
service nginx status
The creation of a basic virtual host for Nginx can be considered complete.
We also need to create a database for the future site, for this we go to the MySQL management console and create a database and a user for it:
mysql
CREATE DATABASE DOMAIN_NAME_db CHARSET utf8; GRANT ALL ON DOMAIN_NAME_db.* TO 'DOMAIN_NAME_usr'@'localhost' IDENTIFIED BY 'XXXXXXXXXXXXXX'; FLUSH PRIVILEGES;
In order to avoid confusion among a large number of databases, we recommend creating a production database for the site according to the following principle, if the site has the site.ru domain name , then the database name should be site_db , and the user name should be site_usr . The password in this case is a random sequence of characters from 15 pieces , we released it earlier in the previous parts of the article.
Providing access to site files to developers
After performing all the operations, it is necessary to give our site user the ability to connect via SSH and FTP :
To issue SSH access, you will need to add a username to the /etc/ssh/sshd_config file , section
AllowUsers DOMAIN_NAME
After, restart SSH
/etc/init.d/ssh restart
And check the status:
/etc/init.d/ssh status
Next, let’s start configuring FTP , since we configured Vsftpd earlier , we just need to add the site username in the system to the files:
/etc/vsftp/vsftpd.chroot_list /etc/vsftp/vsftpd.user_list
After we create a file with a description of user access:
touch /etc/vsftp/vsusers/DOMAIN_NAME
Containing lines:
chroot_local_user=YES local_root=/var/www/DOMAIN_NAME local_umask=022
After, restart the FTP server
/etc/init.d/vsftpd restart
And check the job status:
/etc/init.d/vsftpd status
After completing all the steps, you need to create a password for the site user:
- generate a password of 15 characters:
pwgen 15 1
- assign this password to the site user in the system:
passwd DOMAIN_NAME
A mandatory point for setting up access is to check their correctness, first, let’s go to our machine and check the ability to connect via SSH :
SSH EXTERNAL_IP@DOMAIN_NAME
If the connection went correctly, then there are no problems and you can check the connection via FTP , for these purposes, the easiest way is to use software with a graphical interface, for example FileZilla , install the program on any OS, enter the connection details and check. If the connection fails for some reason, then you probably made a mistake somewhere earlier.
Setting up sending mail from the server
After creating a site and issuing access, we proceed to setting up sending mail from the server. The setup can be considered complete, after receiving the highest spam filter score, the most common SPAM filter at the moment is SpamAssassin .
It is possible to test sending mail from the server using the www.mail-tester.com website , which will analyze your letter and provide all the necessary information and shortcomings. The goal of our setting will be to get 10 points on this resource.
To correctly send mail, we need to create a number of records in the DNS zone of the domain, namely DKIM (public key), SPF, DMARC . You will also need to ask the data center support to specify a PTR record in the reverse zone, or do it yourself if the functionality of your DC provides for such a function.
- Let’s start with DKIM :
To release the DKIM key , we need to install the opendkim-tools package, the package is available in the standard repositories, no additional steps are required to install it
apt update apt install opendkim-tools
Next, let’s create a directory in Exim to store all of our DKIM keys:
mkdir /etc/exim4/dkim
And let’s go to it:
cd /etc/exim/dkim
Next, we generate a public and private DKIM key:
opendkim-genkey -D /etc/exim4/dkim/ -d DOMAIN.RU -s DKIM_SELECTOR -b 1024
where DKIM_SELECTOR is a pointer to look up the public part of the key (usually mail ).
In the current directory, two files will be created mail.private (private key) and mail.txt (public key, which will need to be registered in the DNS zone of the domain. For convenience and further use in the exim4 configuration , rename both files:
mv /etc/exim4/dkim/mail.private /etc/exim4/dkim/DOMAIN.RU.key mv /etc/exim4/dkim/mail.txt /etc/exim4/dkim/DOMAIN.RU.txt
For security reasons, we change the rights to parts of the key and the storage directory:
chown -R root:Debian-exim /etc/exim4/dkim chmod 750 /etc/exim4/dkim chmod 640 /etc/exim4/dkim/DOMAIN.RU.key chmod 640 /etc/exim4/dkim/DOMAIN.RU.txt
Now that we have created the keys, we need to specify the location of the exim4 configuration , for this we add the following lines to the beginning of the /etc/exim4/exim4.conf.template file :
# DKIM settings DKIM_DOMAIN = ${lc:${domain:$h_from:}} DKIM_FILE = /etc/exim4/dkim/${lc:${domain:$h_from:}}.key DKIM_PRIVATE_KEY = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}
We also add the following entries to the remote_smtp block in the /etc/exim4/exim4.conf.template file :
remote_smtp: driver = smtp dkim_domain = DKIM_DOMAIN dkim_selector = $DKIM_SELECTOR dkim_private_key = DKIM_PRIVATE_KEY
where $DKIM_SELECTOR must be substituted with the selector specified when the key was created. The DKIM_DOMAIN and DKIM_PRIVATE_KEY parameters are variables here, and have exactly the same form in the file as described in the article.
I would also like to clarify that the mail server will be able to work with only one DKIM selector in the future. Accordingly, if the signature for the first domain is specified as mail, then it must be the same for all subsequent domains for which the DKIM key will be generated.
After we need to add a TXT record to the DNS zone of the domain, the contents of the record are specified in the DOMAIN.RU.txt file . Recording example:
DKIM_SELECTOR._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWtRUr0hse/k2csH1KtA3YrM2hrSiDxyhRDEV53LvDxjcN8rH5913j0N/5P48kotw0tVKlyaW6x9sJJPs7fjCsuoSQNQUpwjJrPKjH/r7fTBESFMOx6SHMpIC57GadCCmQfGEiI0IHPG0zqakDNUKtLaWBc71BLdRAApsbZ97ooQIDAQAB" ) ; ----- DKIM key DKIM_SELECTOR for DOMAIN.RU
After we delete the key that is no longer needed:
rm -f /etc/exim4/dkim/DOMAIN.RU.txt
Restart exim4 and check its operation:
service exim4 restart service exim4 status
DNS records are updated within 72 hours, but for most DNS servers, the record usually appears within an hour. As a test, we can check if the record exists on the Google DNS servers :
dig @8.8.8.8 +short -ttxt DKIM_SELECTOR._domainkey.DOMAIN.RU
If you receive a public key to the dig request , then all settings are correct. You can also send a letter to any mailbox for which incoming mail is not filtered. From the server, this is done using a command like:
echo "Hello there" | mail -s "world" -r test@DOMAIN_NAME address@recipient
An example of sending a letter for the domain site.ru to the mailbox [email protected] :
echo "Hello there" | mail -s "world" -r [email protected] [email protected]
After you need to find the letter in the mailbox and open its full text version, if you see a line like this there:
dkim=pass
This means that the message header was signed with a private key from the exim4 server side and decrypted using a public key registered in the DNS zone. In other words, everything works correctly.
After we start building SPF records. An SPF record is a record that points to those servers from which you can send mail for a given domain. I will not delve into the work of the SPF record, since our site only needs to send mail to a specific server, so we will create a TXT record in the DNS zone of our domain of the form
DOMAIN.RU. IN TXT "v=spf1 +a +mx ip4:EXTERNAL_IP ~all"
Where EXTERNAL_IP is the external IP address of our server, respectively.
An important point, in DNS zones it is also possible to specify an SPF record , this record is obsolete, in order to use an SPF record, it must have exactly the TXT format.
After specifying the SPF record, run the query using dig and check that everything is correct:
dig TXT DOMAIN_NAME
If your SPF record is among the received records, then everything was added correctly.
Next , let’s create a dmarc policy . It tells the receiving server what to do with the message if the DKIM and SPF records are incorrect. In our case, the minimum policy requirements will be specified using the following TXT record:
_dmarc.DOMAIN.RU. IN TXT "v=DMARC1; p=reject; aspf=r; sp=reject"
We do not recommend disabling the dmarc policy , as recommended by mail-tester , since with the policy disabled, messages may end up in SPAM in some mailboxes.
To check the dmarc indication, we use the command:
dig +short -ttxt _dmarc.DOMAIN.RU
After that, you will need to specify the PTR record. It is used when sending a letter, when the mail server is represented by some name. It should bind the domain specified in the primary_hostname of the exim4 configuration and the external IP of our server. Since in our case the primary_hostname parameter is set to the main domain name of the site, we need to specify a PTR record of the form:
DOMAIN.RU. IN TXT "EXTERNAL IP"
Where DOMAIN.RU is the domain name specified in the primary_hostname of the current exim4 server , and EXTERNAL IP is the external IP address of our server.
This entry can be specified in the DC control panel or, if such a function is not provided, ask to specify the entry through the data center support. The following Dig command is used to check the PTR record :
dig -x DOMAIN_NAME
As soon as all the records are reflected and specified, you can start sending a test letter to mail-tester, we send a letter using the already familiar command:
echo "Hello there" | mail -s "world" -r test@DOMAIN_NAME [email protected]
After all the settings done, we should get 10/10 points for the mail-tester service , which is the highest score.
Final steps
This completes the server setup. You can upload the site code to /var/www/DOMAIN_NAME/data (from the site user!), as well as place the site database in MySQL , specify the database details at the code level and check the site. However, before doing this, be sure to check the permissions and the owner of the files.
Be sure to give developers MySQL access to the site DB user , and SSH and FTP access to the site user on the system (DOMAIN_NAME) .
The advantage of the site working from the user is that code developers have access only to the files of the site and work from their user, while the sites are abstracted from each other at the system level, since each site has its own system user.
The articles already turned out to be quite cumbersome, and therefore I will not describe in detail the configuration of the domain via https, there is a lot of information on the network on how to attach an SSL certificate to Nginx . I can only say that if you need a free certificate, you can issue it using certbot on the server, and then, as an example, set up an unconditional redirect from http to https in the desired Nginx virtual host , for this, add in the virtual host at the beginning:
server { listen 80; server_name DOMAIN_NAME www.DOMAIN_NAME; return 301 https://DOMAIN_NAME$request_uri; }
And below, edit the current configuration for port 80 , changing it to 443 and specifying the assembled certificate and key:
server { listen 443 ssl; server_name DOMAIN_NAME www.DOMAIN_NAME; ssl_certificate /etc/nginx/ssl/DOMAIN_NAME/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/DOMAIN_NAME/private.key;