How to install WordPress on a CentOS 7 / RHEL 7 server

This how-to explains the necessary steps to install WordPress, the popular blogging platform, on your own CentOS 7 server. Hosting WordPress yourself can have several advantages: It gives you full control over your setup, your learn something and in my case it also led to significant improvements in speed and responsiveness of my page.

When I tried to install WordPress on a CentOS 7 server myself, I discovered that many of the how-tos available on the Internet were either outdated or led to configuration issues. Some tutorials were also outright insecure. Therefore I decided to write a blog entry that sums it all up and leads to a reasonably secure and also performant web server for WordPress.

Overview: Install and configure Nginx, PHP, MySQL/MariaDB and SELinux on CentOS 7 to run WordPress

This guide will cover the following topics, which is basically setting up a LEMP-stack (=Linux, Nginx, MySQL, PHP) plus WordPress. In the end some thoughts on performance tuning and security will be given.

Overview and links to the different parts of this blog entry:
Prerequisites
How to install Nginx, the webserver
How to install MariaDB, a MySQL database
How to create a database in MySQL/MariaDB for WordPress
How to install PHP
How to configure Nginx for WordPress
How to install WordPress
How to configure SELinux to make it work with WordPress
How to install phpMyAdmin and secure it to manage the database
Performance and security tweaks for your setup

This guide is for CentOS 7, but it should also work wit Redhat Enterprise Linux RHEL 7. In addition, Fedora Linux should be similar as well.

In the future I will also include how to set up SSL for your server. That being said, this guide comes with no warranty. Feel free to comment below if you have feedback.

Prerequisites

The prerequisites are that you have a server with CentOS 7 installed. The initial server setup is explained very well in this guide.

Installation of NGINX

To install Nginx, you have to activate the Centos 7 EPEL repository:
sudo yum install epel-release

Afterwards, you can install Nginx:
sudo yum install nginx

Start the service:
sudo systemctl start nginx

And open port 80 for http in your firewall and reload the firewall:
sudo firewall-cmd –permanent –zone=public –add-service=http
sudo firewall-cmd –reload

Now you can test your server by opening a browser:
http://(your IP)

If all went well, you should see the default welcome page of Nginx.

The next step is to make sure Nginx is loaded automatically during startup. If you later on do not wish to start Nginx automatically with each boot, just run the command below with “disable” instead of “enable”:
sudo systemctl enable nginx

The configuration of Nginx will be done further below in this blog post.

Installation of MariaDB

To run WordPress, we need a MySQL database. The open version of it is called MariaDB and is fully compatible with MySQL.

Install MariaDB on your server like this:
sudo yum install mariadb-server

After installation, enable it that is loaded automatically and start it:
sudo systemctl enable mariadb
sudo systemctl start mariadb

A very important step is to secure the database. MariaDB is shipped with a standard script that sets a root password and also increases the security of your installation. Run the script, set the root password and accept all other default options on the remaining questions. It asks for example whether to remove anonymous users (-> yes!) or disallow root login from remote (-> yes!). Run the script like this:
sudo mysql_secure_installation

Finally, edit the main configuration file. Here you need to ensure that the database only binds to the local address (i.e. that it cannot be accessed from the Internet) by setting the “bind-address” to 127.0.0.1. Also, the database has no business to look at your data, therefore access to local files should be disabled by setting the local-infile option to 0.
sudo nano /etc/my.cnf

Excerpt with the changes highlighted:
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
bind-address = 127.0.0.1

#Disable access to local files
local-infile=0

Setting up a database for WordPress

Now everything is in place to actually create the database for WordPress that you will need later on. To do this, login to MariaDB with root.
Since we ran the script to secure the db installation before, it should ask for the MariaDB-root password now:
mysql -u root -p

Once you are logged in, you can create the database that is here called “wordpress”:
create database wordpress;

WordPress should not use the root user to access the database, therefore the next step creates another db user called “dbuser” with the password “password”. Please change to a different password and note the exact syntax of the command. The username has to be put written inside of ‘apostrophes’ and the password as well. The command needs to be closed with a semi-colon:

create user dbuser@localhost identified by 'password';

If all went well, MariaDB should respond like this:
MariaDB [(none)]> create user ‘dbuser’@localhost identified by ‘password’;
Query OK, 0 rows affected (0.00 sec)

Now is the time to give this new dbuser access to the database “wordpress” we created in the step before. This is done like this:

grant all on wordpress.* to 'dbuser' identified by 'password';

Now you are done in the database console and can write “exit” to exit:
exit

Installation of PHP

The next step is to install PHP. This is done like this:
sudo yum install php php-mysql php-fpm

Afterwards, you need to edit a few configuration files. Open php.ini first:
sudo nano /etc/php.ini

Scroll to the “paths and directories” section, and make sure the cgi.fix_pathinfo is set to null. This is an important security setting that tells php to only search for and execute the exact filename it receives. With the default setting, PHP would attempt to execute the closest file match. For example, if the file script.php is not found, but a file script.jpg is found, PHP would execute the latter one. This would allow users to run code in PHP that they should not be allowed to. Setting this to 0 solves this problem:

Paths and Directories section:
cgi.fix_pathinfo=0

Afterwards, scroll down to the “file uploads” section of php.ini and increase the maximum upload file size. The default was too low in my case, so I set it to 200 MB. Choose whatever fits you best. This is needed in the future, when you for example import a database via phpMyAdmin or when you upload other files via php:

File uploads section
upload_max_filesize = 200M

Now, open the www.conf configuration file that resides in /etc/php-fpm.d/www.conf:
sudo nano /etc/php-fpm.d/www.conf

Change the listen option like this. This makes sure PHP uses a socket:
listen = /var/run/php-fpm/php-fpm.sock

Further down in the file, uncomment the listen owner and group and make sure it is set like this:
listen.owner = nobody
listen.group = nobody

In addition, change user and group to nginx:
user = nginx
group = nginx

Afterwards, we can start and enable PHP:
sudo systemctl start php-fpm
sudo systemctl enable php-fpm

Configuration of Nginx

Now you need to configure Nginx so that it can handle PHP files. Open the main configuration file in /etc/nginx/nginx.conf. Nginx uses a modular setup. That means you can put the configuration of the servers (similar to virtual hosts in Apache) for example to /etc/nginx/conf.d/* or /etc/nginx/default.d/*. In this tutorial, for the sake of simplicity, I configured Nginx only by using the main configuration file. This has the advantage that you can see every everything in one file only. Please make sure that the /etc/nginx/conf.d/* and /etc/nginx/default.d/* directories are empty.

Now open the main configuration file
sudo nano /etc/nginx/nginx.conf

Remove everything and replace it with the following content. The only part you need to update is to replace YOURHOSTNAME with the hostname of your server, for example domain1.com or example.dyndns.org, in line 45:

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
   
    #set uploads by users to 100 MB
    client_max_body_size 100m;

    #root directory and index definition for http (general)
    root   /usr/share/nginx/html;
    index index.php index.html index.htm;

    #Default server - 
    server {
	listen 80 default;
        return 404;         
       }

    server {
    listen       80;
    server_name  YOURHOSTNAME;

    location / {
        try_files $uri $uri/ /index.php;
    }
    
    error_page 500 502 503 504 /50x.html;    
    location = /50x.html {
        root /usr/share/nginx/html;
    }
    
    error_page 404 /404.html;
    location = /404.html {
        root /usr/share/nginx/html;
    }
      
    #adds expiry date for static content
    location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
        expires 30d;
        add_header Pragma public;
        add_header Cache-Control "public";
     }    

   # location /database {
   #     auth_basic "Please login";
   #     auth_basic_user_file /etc/nginx/pma_pass;
   #	}

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    #prevents .htaccess files from being downloadable
    location ~ /\.ht {
        deny  all;
    }
  
    # Prevents php uploaded in uploaddir from being executed
    #location /uploaddir {
    #    location ~ \.php$ {return 403;}
    #        # [...]
    #        }

}

    # Compresses certain files for better performance
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/javascript 
    application/json application/x-javascript text/xml application/xml 
    application/xml+rss text/javascript application/vnd.ms-fontobject 
    application/x-font-ttf font/opentype image/svg+xml image/x-icon text/x-js;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

}

A few comments on the configuration

These comments are just made for further understanding. If you like you can skip this part, more on the security and performance tweaks can be found at the end of this tutorial.

This allows users to upload files up to 100MB through the server. Adjust the value to your liking:

#set uploads by users to 100 MB
    client_max_body_size 100m;

This defines the root directory for Nginx. It is valid for all servers (servers are like virtual hosts in Apache). This is also the directory where WordPress will be put. The index directive returns the first file it can find. I.e. index.php is served before index.html, for example:

#root directory and index definition for http (general)
    root/usr/share/nginx/html;
    index index.php index.html index.htm;

This configures the server for WordPress. Replace YOURHOSTNAME with the right name, i.e. example1.com or example.dyndns.org, etc:

 server {
    listen       80;
    server_name  YOURHOSTNAME;

The following parts handles how webpages are opened. This is needed for example for WordPress posts that have the title in the URL:

 location / {
        try_files $uri $uri/ /index.php;
    }

Now custom error pages are defined. They are actually rarely used because most of the time you will see the 404 page from WordPress itself:

error_page 500 502 503 504 /50x.html;    
    location = /50x.html {
        root /usr/share/nginx/html;
    }
    
    error_page 404 /404.html;
    location = /404.html {
        root /usr/share/nginx/html;
    }

This part tells Nginx how to handle PHP files and forward them to PHP via the socket connection:

 location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

There are some other parts in the configuration file that I will explain in the performance enhancement section of this post.

Installation of WordPress – First Part

Now the preparation is finished and you finally start the installation of WordPress. We go to the directory /usr/share/nginx/html where the html is stored:

sudo cd /usr/share/nginx/html

and download WordPress:

sudo wget http://wordpress.org/latest.zip

We unzip the file:

sudo unzip latest.zip

and move the contents from the ./wordpress-folder into our html directory:

sudo mv wordpress/* .

Then we remove the empty worpress directory and the downloaded “latest.zip” file:

sudo rmdir wordpress
sudo rm latest.zip

Finally, we set the correct permissions so that Nginx can read and write correctly:

sudo chown nginx.nginx -R .

Now WordPress is unpacked and we are almost ready to go, almost!

SELinux

It took me a long time to figure out why my WordPress was not working as expected until I found out that at this point of the installation you need to take care of SELinux. SELinux is a linux kernel security module that is enabled on default in CentOS. It enforces access control policies and manages which program is allowed to do what. SELinux can be sometimes a pain in the back to configure and I saw some other how-tos that simply disabled it. However, I did not want to simply disable it, but to find the correct configuration. Since most of the documentation is made for Apache, it took a while to figure out what to do.

The trick is that although Nginx got the correct file permissions in the last step above, it still needs to be granted permissions to write and execute for certain directories for WordPress. In my case, I wanted WordPress being able to update itself automatically and it should also be possible to install themes and plugins from the dashboard. This means that Nginx needs to be able to write and execute in some directories. This can be done like this.:

The first two commands tell SELinux where Nginx is allowed to execute php files:

sudo chcon system_u:object_r:httpd_sys_script_exec_t:s0 /usr/share/nginx/html/*.php
sudo chcon system_u:object_r:httpd_sys_script_exec_t:s0 /usr/share/nginx/html/wp-includes/*.php

The next commands allows Nginx to to read and write. Adjust the first command to whereever your “upload” directory is located:

sudo chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 /usr/share/nginx/html/wp-content/uploads
sudo chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 /usr/share/nginx/html/wp-content/upgrade
sudo chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 /usr/share/nginx/html/wp-content/themes
sudo chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 /usr/share/nginx/html/wp-content/plugins
sudo chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 /usr/share/nginx/html/wp-content

Installation of WordPress – Second Part

Now you can install WordPress. Simply open a webbrowser and access your server with it’s IP or hostname:

http://localhost or http://HOSTNAME

Then follow the instructions. Remember to use the database user details we set up above with username “dbuser” and “password”, depending on what you set up.

Installation of phpMyAdmin

When working with WordPress it can come in handy to look at the database from time to time. Especially when making or restoring backups. For this I like to use phpMyAdmin, which is a database administration tool with a web interface.

Install it like this:
sudo yum install phpmyadmin

Then create a link between the directory phpMyAdmin resides in and the root directory for Nginx:
sudo ln -s /usr/share/phpMyAdmin /usr/share/nginx/html

Adjust the permissions for Nxinx and PHP:
sudo chown -Rfv root:nginx /var/lib/php/session

Afterwards, restart PHP:
sudo systemctl restart php-fpm

The next step is important for security reasons: If you have phpMyAdmin installed in the default location, everybody knows that it can be accessed by going to http://yourdomain.com/phpMyAdmin. This is a security risk because somebody could brute force his or her way into your database. Therefore, rename directory. In this example it is renamed to “database”, but better choose something more difficult to guess:
cd /usr/share/nginx/html
sudo mv phpMyAdmin database

In addition, we will password-protect phpMyAdmin from the webserver side. To do this, create a password and hash it through this function.
openssl passwd -1

Now create a new file called pma_pass:
sudo nano /etc/nginx/pma_pass

In this file, add a username of your liking and the encrypted password:
username:encryptedpassword

Then, open the nginx.conf file and uncomment the following section. This tells Nginx to ask for a password when you open ../database:

location /database {
        auth_basic "Please login";
        auth_basic_user_file /etc/nginx/pma_pass;
	}

Now, restart nginx:
sudo systemctl restart nginx

You are now able to access phpMyAdmin at http://yourserver/database

Security and Performance tweaks

First of all, as I already mentioned in the introduction, make sure you have secured your server in general. Important is that your server is up-to-date with the latest software, that the firewall is on and only lets http and ssh through and that SSH itself is secured. How to do this is beyond the scope of this article, but you can have a look at this guide.

For the webserver, I added the following configuration for security and performance. The configuration is already included in the nginx configuration file above.

I defined a default server that listens on port 80 and replies with a 404 message. The reason is that when I later on host several domains with different hostnames, I only want to “activate” the hostnames. Let’s assume my IP is 127.0.0.1 and I host www.example1.com and www.example2.com. If I only enter the IP, a 404 error will come, if I enter any of the domains (=hostnames), the right page will be served.

#Default server 
    server {
	listen 80 default;
        return 404;         
       }

This part of the configuration adds an expiry date for static content. This improves the rating of your page when you run it through speed test websites:

#adds expiry date for static content
    location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
        expires 30d;
        add_header Pragma public;
        add_header Cache-Control "public";
     } 

Just in case you are switching from Apache or import files to your webserver that use .htaccess (Nginx does not use it!), the following prevents that a website visitor can download those files and look at usernames for example:

 #prevents .htaccess files from being downloadable
    location ~ /\.ht {
        deny  all;
    }

The following excerpt prevents php from being executed in the upload directory. Uncomment and change it to the directory you need:

    # Prevents php uploaded in uploaddir from being executed
    #location /uploaddir {
    #    location ~ \.php$ {return 403;}
    #        # [...]
    #        }

Lastly, this part uses compression for text-based files which improves the loading time of your website:

    # Compresses certain files for better performance
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml 
    application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf 
    font/opentype image/svg+xml image/x-icon text/x-js;

Done!

Now everything should be set up. Feel free to test, comment and give feedback!

9 thoughts on “How to install WordPress on a CentOS 7 / RHEL 7 server

  1. lubilux

    I did not have nano installed therefore “sudo nano /etc/my.cnf” did not work for me. I did “sudo vi /etc/my.cnf” instead.

    Also, the following syntax is wrong: create user ‘dbuser’@localhost identified by ‘password’;

    The correct one is: create user dbuser@localhost identified by ‘password’;

    Cheers for the tutorial btw. I am still going on.

  2. Chris

    Thanks for the feedback. I corrected the command in the blog entry. It should be the normal apostrophe sign, but somehow WordPress replaced it. When you copied and pasted it, it gave an error. I fixed it now by showing the command as a code, in addition, for the dbuser, as you pointed out, the apostrophes are not needed… Good luck further! :)

  3. lubilux

    I guess there are some errors within the sudo nano /etc/nginx/nginx.conf file you have given.

    That is what I am getting after trying to restart nginx with “sudo systemctl restart nginx”:

    Job for nginx.service failed because the control process exited with error code. See “systemctl status nginx.service” and “journalctl -xe” for details.

    Further info:

    Process: 2581 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=1/FAILURE)
    Failed to start The nginx HTTP and reverse proxy server.

  4. Chris

    What output do you get when you run “sudo nginx -t” (It checks the syntax of the .conf file)?

  5. lubilux

    That’s what I get with “sudo nginx -t”

    nginx: [emerg] unknown directive “application/json” in /etc/nginx/nginx.conf:106
    nginx: configuration file /etc/nginx/nginx.conf test failed

  6. lubilux

    My bad. I figured out that I had no “}” at the end :/

    Nginx restarted without any problem after I added it. Better delete these comments :)

  7. lubi

    Great tutorial! I have managed to finish it and make the wordpress work. Hoever, I cannot install any plugin:

    Could not create directory.

    I guess further measures are needed for SELinux.

  8. lubi

    I have found it. I think the following command did not work: “sudo chown nginx.nginx -R .”

    I tried the following and it worked: “chown -R nginx:nginx /usr/share/nginx/html/”

Leave a Reply

Your email address will not be published. Required fields are marked *