Updated on 25.5.2023

How to install single node WordPress LEMP CentOS 8

WordPress LEMP CentOS8

WordPress is a popular CMS that can allow you to configure static or dynamic websites inclusive of e-commerce. If you are inclined to install for speed the LEMP stack would be preferable as Nginx performs approximately 2.5 faster than Apache. LEMP stands for Linux, Nginx (Engine-X), MySQL and PHP.

Along with performance benefits and happier visitors, speed also impacts your SEO rankings. In this tutorial, we will be installing WordPress LEMP CentOS 8 with Nginx, MySQL 8, and PHP-FPM, an alternative PHP FastCGI implementation which is significantly faster than mod_PHP. We will also be proceeding through a few suggestions and alternative configurations for hardening, speed, and scalability.

Step 1 Point DNS or Hosts File

You may wish to begin with your DNS settings. However, you can also begin without purchasing a domain name. A hosts file can be configured depending on your local machines operating system so you can view your web content from a browser before DNS propagation takes place.

Typically for Mac and Linux users the following /etc/hosts file structure will map your IP address to the domain name. Windows and alternative OS users should review depending on the OS version.

vi /etc/hosts example1.com www.example1.com

Step 2 Install Nginx

Nginx Logo Transparent

Nginx is a highly performant open-source service that can be configured for a web server, caching, load balancing, media streaming and much more.

We will be keeping things fairly simple by installing Nginx as a web server.

yum install nginx -y

Once installed Nginx can be started with the following command.

systemctl start nginx

To enable Nginx to start at system boot ensure the service is enabled.

systemctl enable nginx

Ensure the service is functioning properly by checking the status.

systemctl status nginx

Check you are running the desired version.

nginx -v

RHEL 8 / CentOS 8 defaults do not have firewalld configured to serve web traffic, run the following commands to allow web server traffic for HTTP and https with permanent rules that will persist after a reboot.

firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https

Reload the firewall daemon for the changes to occur.

firewall-cmd --reload

Step 3 Install Percona Server MySQL 8

Percona MySQL 8

WordPress LEMP CentOS 8 installations frequently run forks of MySQL inclusive to MariaDB and Percona Server.

The default Red Hat 8 Centos 8 repositories are currently prepared to install MariaDB and MySQL 8 typically outperforms MariaDB, yet Percona Server typically outperforms the MySQL Community Server while providing open-source enterprise features.

MariaDB can be a replacement for MySQL and it is applicable for WordPress installations, yet it is also worth noting that MariaDB has some enhanced features, which do not exist in MySQL and migration back to MySQL might not always work. Many of the unique MariaDB features and storage engines that make MariaDB stand out will not be of use to a WordPress installation.

MariaDB and Percona Server are heavily documented and both companies provide managed database support.

Percona toolkit, in general, has many useful open-source tools depending on which path you choose. Percona Xtrabackup and Mariabackup hot backups can be very useful to avoid downtime without paying enterprise costs.

As this is a single node installation we will be installing the database directly on the webserver. In many environments, it would be preferable to employ a decoupled architecture and have MySQL on its own instance. This way scaling web servers behind a load balancer and performance tuning would be an easier task upon growth.

Install the Percona Server repository.

yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm

Setup the repository.

percona-release setup ps80

Disable the following as prompted.

* Disabling all Percona Repositories On RedHat 8 systems it is needed to disable dnf mysql module to install Percona-Server Do you want to disable it? [y/N] Y

Install Percona Server.

yum install percona-server-server -y

Make a copy of your my.cnf file.

cp /etc/my.cnf /etc/my.cnf.bak

Open the my.cnf file and uncomment the following line as MySQL 8 has changed the default value. Eventually, the community will update the mysqlnd client library to use the new encryption method and this configuration will no longer be required. At this current time, WordPress will throw database connection errors on authentication if you haven’t uncommented this out before user creation.

vi /etc/my.cnf 



Start the MySQL service.

systemctl start mysql

Enable the MySQL service to start at boot.

systemctl enable --now mysqld

Check the status of the MySQL service.

systemctl status mysql

Obtain the temporary password from the mysqld.log.

grep "temporary password" /var/log/mysqld.log

Proceed with secure installation.


Follow the prompts.

Set root password? Y 
Enter password for user root: <Paste-copied-password>
Estimated strength of the password: 100
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : Y
Remove anonymous users? Y 
Disallow root login remotely? Y 
Remove test database and access to it? Y
Reload Privilege tables now? Y

Enter the MySQL shell.

mysql -u root -p

Create the WordPress database.


Create the WordPress user inclusive of the desired password.

CREATE USER wordpressuser@localhost IDENTIFIED BY 'enter-password-here';

Grant permissions to the user on the WordPress database.

GRANT ALL ON wordpress.* TO wordpressuser@localhost;

Proceed to flush the privileges.


Exit the shell.


Step 4 Install PHP & PHP-FPM

For a clean and easily upgradable PHP configuration, we recommend using the default repository. You may consider newer versions in more bleeding edge repositories; though, it is also worth noting that alternative repositories may include much longer service names and alternative upgrade paths. It is also best practice to review deprecations for the version of PHP you are migrating from and to.

To install PHP and related modules run the following command.

yum install -y php php-mysqlnd php-fpm php-opcache php-gd php-xml php-mbstring

After the services and modules are installed start PHP-FPM.

systemctl start php-fpm

Ensure the service is enabled at boot.

systemctl enable php-fpm

Check the service status.

systemctl status php-fpm

The default PHP-FPM configuration file is set to run as the apache user, locate and alter the following entries as shown below.

vi /etc/php-fpm.d/www.conf
user = nginx
group = nginx
listen.owner = nginx
listen.group = nginx

Reload PHP-FPM for the changes to occur.

systemctl restart nginx php-fpm

Step 5 Install WordPress

WordPress Transparent Logo

Installing the most up to date version of WordPress is critical for security, the same can be said for themes or plugins. Automatic updates may sound appealing; though, you may find a manual administrative review of backups and troubleshooting is best should an update break something.

Change to the Nginx root directory.

cd /usr/share/nginx/html

Download a copy of the latest WordPress installation.

wget https://wordpress.org/latest.tar.gz

Tar is no longer installed by default on RHEL 8 / CentOS 8 and will need to be installed.

yum install -y tar

Use tar to extract the file contents.

tar -zxvf latest.tar.gz

Clean up the unnecessary file:

rm latest.tar.gz

Find all of the directories in the defined path and update octal permissions.

find /usr/share/nginx/html/wordpress -type d -exec chmod 755 {} ;

Find all of the files in the defined path and update octal permissions.

find /usr/share/nginx/html/wordpress -type f -exec chmod 644 {} ;

Step 6 Nginx and wp-config.php Configuration

Next for our WordPress LEMP CentOS 8 installation we will be proceeding through Nginx and wp-config.php settings inclusive to a few security checks. This configuration is not for SSL as we have alternative documentation for SSL configurations in our tutorial Install Lets Encrypt SSL Nginx.

You should also take note that A self-signed certificate may be effective temporarily, but it would not provide the SEO benefits of a trusted certificate. Along with SEO penalties, self-signed certificates will cause warnings in the browsers for your visitors that can cause them to leave your site entirely.

Should you find yourself facing various SSL related errors the Really Simple SSL plugin can be a very handy quick fix.

Exclude XML-RPC support from your main nginx.conf as it is a common DOS attack location.

vi /etc/nginx/nginx.conf 
location = /xmlrpc.php {
   deny all;
   access_log off;
   log_not_found off;
   return 444;

Copy the following snippet and make adjustments for your root directory or server_name accordingly.

vi /etc/nginx/conf.d/default.conf 
server {
   listen 80;
   server_name example1.com www.example1.com;

   # note that these lines are originally from the "location /" block
   root /usr/share/nginx/html/wordpress;
   index index.php index.html index.htm;

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

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

Ensure Nginx and PHP-FPM pick up the changes.

systemctl restart nginx php-fpm

Change directories to your WordPress installation.

cd /usr/share/nginx/html/wordpress

Copy wp-config-sample.php file to wp-config.php.

cp wp-config-sample.php wp-config.php

Change the ownership of wp-config.php to Nginx.

chown nginx.nginx wp-config.php

Run the following curl command to generate salts for your wp-config.php.

curl -s https://api.wordpress.org/secret-key/1.1/salt/

Obtain the matching output as follows and replace the corresponding lines in your wp-config.php file.

define('AUTH_KEY', ';]wL<]6QUL =7|MU$b;01u?;+-4|pDCRU9zHjGs2YwSWiWR@2n3t+lfua-+s^bi[');
define('SECURE_AUTH_KEY', 'G+Y[;~YIn|2V.TIOxN6Quo<V_4s2G=0qI|6^}fI|3OBg8Q9~v[]XO!Upg,dl)OHu');
define('LOGGED_IN_KEY', 'h~Jo+S6xF^v_6>#ZZU,,Q9B [%9>nO][email protected],~=3');
define('NONCE_KEY', 'S_5Z?eY~@H: f;|?Q~RGkaQsJFI0p<M}%#V{r~N##M:jw=lxTd6uzl sO31ay-xM');
define('AUTH_SALT', '|`fx},S-Y,i6l]k.F<SiJn4@-2qs|z4*H<ehs%1{eb-9R|4A 6nZ>3s#-4rMkeD@');
define('SECURE_AUTH_SALT', '-H6f(A^2!=JE+El(hXLSQd*gB&Gq{*wl`o*Xv,N|HMD{-.o6{8p~xTvXE|+$YK|L');
define('LOGGED_IN_SALT', 'axF3sx3X#hN<,8^2(btXk;}A[+z/O2*LV[A?Y++!0r3S_Wk`ryD;irmM/8jbei8Q');
define('NONCE_SALT', '2uk&qo?P|+3$nQjsLs:L<2I|#q}g~80W!*Xs-g|IT+o~n[[P_]7z>%uT{+lbZ>:o');

Open your wp-config.php and modify to match your credentials used when we were configuring the MariaDB service.

vi wp-config.php
define('DB_NAME', 'wordpress');

/** MySQL database username */
define('DB_USER', 'wordpressuser');

/** MySQL database password */
define('DB_PASSWORD', 'password-you-provided');

/** This entry will allow you to update, install plugins or themes on WordPress without using FTP **/
define('FS_METHOD', 'direct');

Restart the PHP-FPM and Nginx services to pick up the recent changes.

systemctl restart php-fpm nginx

From here you can use your browser to reach the defined domain name. You may need to clear your caches or use another browser to initially view your WordPress installation.

Wordpress LEMP Centos 8
WordPress LEMP CentOS 8

Additional and Optional Configurations

WordPress specific security can be found within the WordPress Codex and should be a standard go-to for such reviews.

The security recommendations are meant to be a starting point. It is also worth noting that at default SELinux is not configured on our servers and this tutorial will not be covering it. Red Hat recommends that SELinux be configured and your team should consider their level of familiarity with the topic before proceeding.

Our tutorial on securing your cloud server may also be of assistance when thinking more granularly about your security options.


Fail2ban should be configured on all running nodes and WordPress happens to have a very handy plugin that can add additional brute force protection to your environment.

We can find brute force attempts over SSH in /var/log/secure with the following syntax.

# tail -n 10 /var/log/secure | grep -i Failed
Oct 17 08:43:39 centos-1cpu-1gb-fi-hel1 sshd[9945]: Failed password for invalid user avanthi from port 59626 ssh2
Oct 17 08:43:44 centos-1cpu-1gb-fi-hel1 sshd[9947]: Failed password for invalid user avanthi from port 63057 ssh2

Here we can find failed attempts sorted on “Failed” by IP address. It appears this IP address is being abusive.

# tail -n 100000 /var/log/secure | grep -i 'Failed' | awk {'print $13'} | uniq -c | sort -nr
1 65088
1 64964

We could proceed to block this IP address manually after performing a whois lookup or ideally we can install Fail2ban and make the assumption that after that many attempts it is likely malicious. It can be difficult and time-consuming to report every single abusive IP address to its source, though if you have the time please do so.

Additional to this tutorial we also have the following our Fail2ban Centos, Fail2ban Debian, and Fail2ban Ubuntu.

Outside of the actual Fail2ban service, you may also find the Fail2ban Plugin of additional use.

Fail2ban Service Installation Centos 8 for SSH jail.

Install the EPEL repository.

yum install -y epel-release

Install the Fail2ban service.

yum install -y fail2ban

Ensure fail2ban is enabled at boot.

systemctl enable fail2ban

Start the fail2ban service.

systemctl start fail2ban

Open your fail2ban jail configuration file.

vi /etc/fail2ban/jail.conf

Take note of important parameters, in this case, we recommend the default parameters for the SSH block.

bantime = 10m
findtime = 10m 
maxretry = 5

Comment out and replace with firewalld instructions unless you are using iptables.

#banaction = iptables-multiport
#banaction_allports = iptables-allports
banaction = firewallcmd-multiport
banaction_allports = firewallcmd-allports

There are two SSHD blocks, under the uncommented SSHD entry that enable the service.

enabled = true

After changes to your jail.conf have been made, ensure the service picks up the alterations with a restart.

systemctl restart fail2ban

Find the status of a failed and banned IP address.

fail2ban-client status

Find the status of a failed and banned IP address by service.

fail2ban-client status sshd

We can see that our Fail2ban configuration is in place and check that we are no longer being attacked.

# tail -n 100 /var/log/secure | grep -i failed | tail -n 5
Oct 17 08:43:17 centos-1cpu-1gb-fi-hel1 sshd[9939]: Failed password for invalid user avanthi from port 65182 ssh2
Oct 17 08:43:25 centos-1cpu-1gb-fi-hel1 sshd[9941]: Failed password for invalid user avanthi from port 52740 ssh2
Oct 17 08:43:33 centos-1cpu-1gb-fi-hel1 sshd[9943]: Failed password for invalid user avanthi from port 55619 ssh2
Oct 17 08:43:39 centos-1cpu-1gb-fi-hel1 sshd[9945]: Failed password for invalid user avanthi from port 59626 ssh2
Oct 17 08:43:44 centos-1cpu-1gb-fi-hel1 sshd[9947]: Failed password for invalid user avanthi from port 63057 ssh2
# date
Thu Oct 17 11:02:19 UTC 2019

Fail2ban SFTP

The following can test the output of pattern matching to ensure your regex is capturing all the entires.

fail2ban-regex /var/log/secure /etc/fail2ban/filter.d/sshd.conf

Enabling the VSFTPD Fail2ban jail isn’t required as in this configuration VSFTPD connections are using the same SSHD pattern matching. However, should you enable the VSFTPD jail, the following will test your filter pattern.

fail2ban-regex /var/log/secure /etc/fail2ban/filter.d/vsftpd.conf


In the aforementioned WordPress LEMP CentOS 8 configurations we made an alteration to wp-config.php to bypass FTP warmings in WordPress, you may optionally have the desire to fully configure FTP. FTP is inherently insecure as it is unencrypted, it’s best practice to use SFTP.

VSFTPD Installation

Install the VSFTPD service.

yum install -y vsftpd

Ensure the VSFTPD service is started at boot time.

systemctl enable vsftpd

Start the VSFTPD service.

systemctl start vsftpd

Check VSFTPD status to ensure everything is running smoothly.

systemctl status vsftpd

Generate an SSL Certificate for encrypting connections.

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/vsftpd/vsftpd.pem -out /etc/vsftpd/vsftpd.pem
vi /etc/vsftpd/vsftpd.conf

Ensure the following settings, they should be the default.


The following entries will need to be added manually.


Restart the service to pick up the alterations.

systemctl restart vsftpd

The firewall will need to have the ports opened.

firewall-cmd --permanent --add-port=20-21/tcp

Reload the firewall to ensure changes are updated.

firewall-cmd --reload

Set SFTP User and Permissions

Create the new FTP user where /usr/share/nginx/ is the home folder for the user.

useradd -d /usr/share/nginx/ wpftp

Set the password for the wpftp user.

passwd wpftp

Create the www-data group.

groupadd www-data

Add the wpftp user to the www-data group.

usermod -aG www-data wpftp

Open nginx.conf and update the Nginx user.

vi /etc/nginx/nginx.conf 
user wpftp;

Test the Nginx configuration, you can avoid the reload until your permissions are set.

nginx -t

Open www.conf and update the PHP-FPM user.

vi /etc/php-fpm.d/www.conf
user = wpftp
group = www-data 
listen.owner = wpftp
listen.group = www-data

Test the PHP-FPM configuration, you can avoid reloading until your permissions are in place.

php-fpm -t

Set ownership with the new user and group.

chown -R wpftp:www-data /usr/share/nginx/html

Update ownership for /var/lib/nginx.

chown -R wpftp:www-data /var/lib/nginx

Ensure log permissions are in place.

chown -R wpftp:www-data /var/log/nginx

Change ownership for PHP-FPM listening on the UNIX socket.

chown wpftp:www-data /var/run/php-fpm/www.sock

After you have confirmed permissions and configurations pass tests proceed to restart Nginx and PHP-FPM.

systemctl restart nginx
systemctl restart php-fpm

WordPress Login Page

It is recommended that your WordPress login page be altered as a form of security through obscurity.

The following is an example of attempts on your WordPress login page.

# tail -n 5000 /var/log/nginx/access.log| grep -i wp-login
<HiddenIPAddress> - - [17/Oct/2019:06:03:23 +0000] "GET /wp-login.php HTTP/1.1" 200 3253 "http://www.example1.com/wp-admin/install.php?step=2" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0" "-"
# tail -n 5000 /var/log/nginx/access.log| grep -i wp-login | awk {'print $1'} | sort -nr | uniq -c
5 <HiddenIPAddress>

It is feasible to alter the location of your wp-login page manually but this would require altering the core WordPress files and should be avoided. I am going to recommend the WPS Hide Login Plugin.

Stop Unwanted Bots and Crawling with robots.txt

You may find that your server is having a significant amount of traffic from various bots for search engines you don’t care much about ranking with. Perhaps you only care about the Googlebot and want no other crawling to reduce unnecessary traffic. These restrictions can be made with your robots.txt file. There are additional configurations for speeding up indexing and search order that we will not be covering today that could also be considered.

The following are a few one-liners that may assist you in tracking down aggressive bot locations.

# cat /var/log/nginx/access.log | grep -i bot
# cat /var/log/nginx/access.log | awk {'print $1'} | sort -nr | uniq -c
154 <HiddenIPAddress>

A simple whois lookup on the IP address can assist you in drilling down the source of the IP.

# whois

For example, you could restrict the following Google bots and disallow all others. For a proper view of the current Google user-agents please visit this Google support article. 

User-agent: Googlebot 
User-agent: Googlebot-News
User-agent: Googlebot-Image
User-agent: Googleboot-Video 
User-agent: Mediapartners-Google
User-agent: AdsBot-Google
Allow: /

User-agent: *
Disallow: /

It is also worth noting that Google has released a Testing Tool for robots.txt that provides verification of your configurations.

Allows access to all and block the Baiduspider.

User-agent: Baiduspider
User-agent: Baiduspider-video
User-agent: Baiduspider-image
Disallow: /

User-agent: *

The following entry restricts crawling on the wp-admin page and allows Google Search Console to crawl the admin-ajax.php file or it will throw errors.

User-agent: *
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php

This entry tells bots how to search for your sitemap which makes the crawler more efficient.

Sitemap: https://yourdomain.com/sitemap.xml

Commonly Used Services, Plugins and Monitoring


CloudFlare has CDN features with additional security benefits with plans that start in a free tier.

When configuring Cloudflare you may also wish to restrict to the Cloudflare IPs as it would still be feasible to DDoS the server directly bypassing Cloudflare.

The –with-http_real_pi_module and log_format are now configured by default and a list of prefixes will need to be updated regularly based on the Cloudflare IP list.

The following entries can be placed in the main nginx.conf file or be abstracted into an individual configuration file.

set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;

Use either of the following two entries.

real_ip_header CF-Connecting-IP;
#real_ip_header X-Forwarded-For;

Wordfence has security features inclusive to theme and plugin scanning that starts in a free tier.

Entirely free open-source database monitoring can be configured with Percona Monitoring and Management (PMM) this tool is based on Grafana.

New Relic

Application-level monitoring through New Relic starts in a free trial mode then after expiration turns into a Lite account. APM lite includes similar features to Essentials paid plan, apart from transactions, traces, and alerts.

In a WordPress CentOS 8 LEMP environment and many other CMS e-commerce environments, it is best practice to have layers of caching. These configurations can become very complicated, yet have great performance gains.

Varnish Cache Logo Transparent

A page cache such as Varnish allows you to store requests in memory instead of reaching for a location on disk from the backend if the cache is empty Varnish will have to read the data from disk into memory.

When a request is made to WordPress for the first time a query is performed on the database instance and when Redis is configured that query is cached for later use to prevent additional reads to the database.

Redis Logo Transparent

Redis and Varnish can be installed on the single node server even though a recommended installation should decouple the services to their own instances for scalability and high availability.

There are a number of ways to configure your caching layers situational to your session handling and high availability strategy. Considerations are inclusive to excludes and purge rules which can be circumstantial depending on Woocommerce, static or dynamic WordPress environments. The Redis Cache Plugin and Vcaching Plugin could be considered alongside installations that match your environmental needs.


Congratulations! You have completed a single node WordPress LEMP CentOS 8 installation and should have found a few different options for drilling down growth points, security, and performance.


  1. Wow, great… This is what i want to search, not only about installing LEMP but you also give how to secure your server from being attack. It would be better if you made a post about installing varnish / redis too. And if may i know, is this tutorial also work for debian?

  2. Janne Ruostemaa

    Hi there, thanks for the comment. While we do not currently have tutorials on installing Varnish or Redis we’ll certainly keep the request in mind. It should be possible to also install all the components used here on Debian but the steps will differ somewhat due to the different package manager.

  3. Hi, I am following these instructions to the letter, except I won’t use a domain right now; all my learning will be done via IP access. I stumbled across a problem when following these instructions.

    “Copy the following snippet and make adjustments for your root directory or server_name accordingly.

    vi /etc/nginx/conf.d/default.conf”

    Problem is, there was no ‘default.conf’ file inside that folder, only ‘php-fpm.conf’; plus the snippet contains domains used as example, but as I said, I am not using any domains at the moment. What shoud I put on ‘server_name ‘?

  4. Janne Ruostemaa

    Hi Filipe, thanks for the question. Opening the text editor using the example command will create the default.conf file. As for the server_name, you can use the cloud server IP address as if it were a domain name until you have an actual one. Simply enter the IP address to the default.conf file.

  5. Thank you for this guide.

    One thing I ran into was after starting the wp-admin page and submitting my information, I got an error: Fatal error: Uncaught Error: Call to undefined function json_decode() in /usr/share/nginx/html/wordpress/wp-includes/….

    The resolution was to run ‘yum install php-json’

  6. Janne Ruostemaa

    Hi there, thanks for the comment and the addition!

Leave a Reply

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

Back to top