Drupal & CentOS 7 LEMP (Linux, Nginx, MariaDB, php-fpm)

About installation of CentOS, Drupal and company.

CentOS 7

Install CentOS itself and then yum update or yum upgrade.

For your convenience install Midnight Commander:

# yum install mc

Usually I'm using VirtualBox with two ethernet adapters - NAT and Host-only. In than case second adapter doesn't start automatically. To enable:

/etc/sysconfig/network-scripts/ifcfg-enp0s8

ONBOOT=yes

firewalld is blocking access. You can disable it at all or add services (rules) like this:

# firewall-cmd --zone=public --permanent --add-service=http
# firewall-cmd --zone=public --permanent --add-service=ftp
# firewall-cmd --reload

There are problems: old versions of MariaDB and PHP while Nginx is missing.

Nginx installation

a) by adding the "native" repository - file /etc/yum.repos.d/nginx.repo:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1
# rpm --import http://nginx.org/keys/nginx_signing.key

b) from EPEL:

# yum install epel-release

By the way EPEL will be needed later.

Anyway:

# yum install nginx

Service is disabled. To enable:

# systemctl start nginx
# systemctl enable nginx

MariaDB installation

To add repository:

# curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash

Or, if it doesn't work, one can create a repository file based on https://mariadb.org/download/?t=repo-config

E.g. /etc/yum.repos.d/mariadb.repo:

# MariaDB 10.4 CentOS repository list
# https://mariadb.org/download/
[mariadb]
name = MariaDB
# rpm.mariadb.org is a dynamic mirror if your preferred mirror goes offline. See https://mariadb.org/mirrorbits/ for details.
# baseurl = https://rpm.mariadb.org/10.4/centos/$releasever/$basearch
baseurl = https://mirror.docker.ru/mariadb/yum/10.4/centos/$releasever/$basearch
module_hotfixes = 1
# gpgkey = https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgkey = https://mirror.docker.ru/mariadb/yum/RPM-GPG-KEY-MariaDB
gpgcheck = 1

Install:

# yum install MariaDB-server MariaDB-client

Turn it on:

# systemctl start mariadb
# systemctl enable mariadb

Secure:

# mysql_secure_installation

Initially root password is empty, Enter, change (set) it, others answers are Y.

Database and user for Drupal are need to be created. Go to the MariaDB CLI - mysql or mysql -p depending on your settings. Create DB drupal, user drupal (password better be stronger) and grant privileges by series of queries:

CREATE DATABASE drupal;
CREATE USER drupal@localhost IDENTIFIED BY 'drupal_password';
GRANT ALL PRIVILEGES ON drupal.* to drupal@localhost;
FLUSH PRIVILEGES;

Exit - exit or \q

PHP installation

# yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm

If epel-release is not yet added then it will be "pulled" now.

# yum install yum-utils
# yum-config-manager --enable remi-php73

(or any other version required)

Warning! yum install php also installs Apache (httpd) which is not needed.

Base:

# yum install php-fpm

One can (must!) yum install php-cli.

Example of PHP extensions installation:

# yum install php-gd php-mysqlnd php-curl php-mbstring php-xml

Problem: nginx running under nginx user, but php-fpm - apache. Let's add www-data user In "Ubuntu style":

# useradd -m -d /var/www -s /sbin/nologin -r -p "*" www-data

GID 33 is occupied so just -r. It is possible to make /var/www manually without -m and then chown/chmod.

FTP

From EPEL:

# yum install proftpd proftpd-utils

Create FTP user (substitute uid and gid of system user created before):

# ftpasswd --passwd --file=/etc/proftpd.d/ftpd.passwd --name=www-data --uid=996 --gid=994 --home=/var/www --shell=/bin/false

Important! shell is exactly /bin/false, not /sbin/nologin. chown ftp:ftp and chmod 400 on password file if you wish.

SELinux is being stubborn:

# setsebool -P allow_ftpd_full_access=1

Edit /etc/proftpd.conf.

"For the sake of beauty":

User ftp
Group ftp

Make comment or remove:

  • AuthPAMConfig proftpd
  • AuthOrder

Insert into server configuration:

AuthUserFile /etc/proftpd.d/ftpd.passwd
AuthPAM off
AuthOrder mod_auth_file.c

"Webmin-compatible" add to Global section:

RequireValidShell off

Comment Umask or make sure its value is 022 so the rights to new files and directories will be 644 and 755 respectively.

Now it's time for long and hard setting up of services.

PHP-FPM

/etc/php-fpm.d/www.conf

user = www-data
group = www-data
listen = /run/php-fpm/www.sock
listen.owner = www-data
listen.group = www-data

chown -R on /var/lib/php and /var/log/php-fpm

There is warning if group other than root permitted to write into logging directory.

/etc/php.ini

cgi.fix_pathinfo=0

If you wish:

expose_php = Off
memory_limit
display_errors
display_startup_errors
post_max_size
upload_max_filesize
max_file_uploads
allow_url_fopen = Off

Enable and start service.

Nginx

/etc/nginx/nginx.conf

user www-data;

https://www.nginx.com/resources/wiki/start/topics/recipes/drupal/
GET FIXED

Configure Nginx e.g. /etc/nginx/conf.d/drupal.conf:

server {
    server_name drupal.example.com;
    listen 80;
    root /var/www/drupal; ## <-- Your only path reference.

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        allow 192.168.0.0/16;
        deny all;
    }

    location ~ \..*/.*\.php$ {
        return 403;
    }

    location ~ ^/sites/.*/private/ {
        return 403;
    }

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;
    }

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;
    }

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;
    }

    location / {
        # try_files $uri @rewrite; # For Drupal <= 6
        try_files $uri /index.php?$query_string; # For Drupal >= 7
    }

    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }

    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;
    }

    # In Drupal 8, we must also match new paths where the '.php' appears in
    # the middle, such as update.php/selection. The rule we use is strict,
    # and only allows this pattern with the update.php front controller.
    # This allows legacy path aliases in the form of
    # blog/index.php/legacy-path to continue to route to Drupal nodes. If
    # you do not have any paths like that, then you might prefer to use a
    # laxer rule, such as:
    #   location ~ \.php(/|$) {
    # The laxer rule will continue to work if Drupal uses this new URL
    # pattern with front controllers other than update.php in a future
    # release.
    location ~ '\.php$|^/update.php' {
        # Security note: If you're running a version of PHP older than the
        # latest 5.3, you should have "cgi.fix_pathinfo = 0;" in php.ini.
        # See http://serverfault.com/q/627903/94922 for details.
        # regex to split $uri to $fastcgi_script_name and $fastcgi_path
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        # Check that the PHP script exists before passing it
        try_files $fastcgi_script_name =404;

        # Bypass the fact that try_files resets $fastcgi_path_info
        # see: http://trac.nginx.org/nginx/ticket/321
        set $path_info $fastcgi_path_info;
        fastcgi_param PATH_INFO $path_info;

        fastcgi_index index.php;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        include fastcgi_params;

        # Block httpoxy attacks. See https://httpoxy.org/.
        fastcgi_param HTTP_PROXY "";
        fastcgi_intercept_errors on;

        # PHP socket location.
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    # Fighting with Styles? This little gem is amazing.
    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6
    location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7
        try_files $uri @rewrite;
    }

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
        try_files $uri /index.php?$query_string;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }
}

So the "sources" are in /var/www/drupal directory.

Since we have changed user, the service most likely needs to be restarted.

# systemctl restart nginx

SELinux

Again (and again)!

Install utilities:

# yum install policycoreutils-python

Allow write:

# semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/drupal/sites/all(/.*)?"    
# semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/drupal/sites/default/files(/.*)?"

Apply:

# restorecon -r /var/www/drupal/sites

Check if correct context has been established:

# ls -Z /var/www/drupal/sites/all
# ls -Z /var/www/drupal/sites/default/files

Conclusion

Generally speaking this completes the stack setup - site should work. Next comes the creative stage for which my guide no longer apply.