Skip to main content

Chapter 20 · Deploying a Dynamic Website with the LNMP Stack

Chapter Overview

The LNMP stack—Linux + Nginx + MySQL + PHP—is a popular solution for building dynamic websites. It is free, efficient, highly scalable, and lightweight, making it a favorite among developers. In this chapter I compare installing services from source versus RPM packages, then walk through compiling and installing each component from source. Finally, I’ll deploy the WordPress blog system to verify the environment.

As the final chapter of this book, my hope is not only that you land a great job after finishing your study, but also that you build your own blog or forum to share the Linux experience and techniques you gain at work—contributing to the open‑source community and helping move it forward.

20.1 Source packages

Back in Chapter 1, I noted that before RPM (Red Hat Package Manager) became prevalent, Linux administrators often had to install software from source tarballs—a time‑consuming and patience‑testing process. Upgrading or removing software also required careful handling of inter‑package and library dependencies. This demanded solid theory, strong hands‑on skills, and a lot of patience to install even a single program. Because most readers are new to operations, earlier chapters relied on repositories to install software.

Even today, many programs are only available as source. If you only know how to use dnf, you’ll be stuck when software ships only as source: either wait for a third party to wrap it as an RPM or look for an RPM‑based alternative. That skills gap can become a liability in day‑to‑day work.

To ensure you won’t be held back, this section shows how to install from source. Installing from source has two clear advantages:

Portability is excellent. Source packages can be built on virtually any Linux distribution, whereas RPMs are compiled for a specific distro/architecture and expect a matching runtime.

Because compilation happens on the target host, the build can better adapt to the system environment, often yielding better performance and optimization than generic RPM builds—in effect, a “tailored suit” for your system.

As a rule of thumb: if a repository package exists, use dnf; if not, look for a suitable RPM; if nothing’s available, build from source. Despite appearances, the source‑build workflow boils down to four or five steps:

Step 1: Download and extract the source tarball. For network transfer efficiency, source is typically archived and compressed (for example, .tar.gz or .tar.bz2). Extract it, then change into the directory:


root@linuxprobe:~# tar xzvf FileName.tar.gz
root@linuxprobe:~# cd DirectoryName

Step 2: Run the configure script to probe the system and generate a Makefile. You can add --prefix to set a custom install path for better control.


root@linuxprobe:~# ./configure --prefix=/usr/local/program

Step 3: Build the binaries from source using the Makefile’s rules:


root@linuxprobe:~# make

Step 4: Install the compiled program. If you used --prefix, installation goes under that path; otherwise the default is usually under /usr/local.


root@linuxprobe:~# make install

Step 5: Clean up temporary build files to avoid wasting disk space:


root@linuxprobe:~# make clean

You may wonder why configure and make take so long compared with an RPM install. In RHCA’s RH401 exam you’ll learn to build RPMs: an RPM is essentially a source tree plus a set of build/install rules pre‑tuned for a specific system and architecture. Vendors often publish multiple RPMs for different architectures (i386, x86_64, etc.). Source authors, by contrast, want their software to compile on as many systems as possible, so configure checks your environment and decides on a workable plan—more checks, more time.

20.2 The LNMP dynamic website stack

LNMP combines Linux, Nginx, MySQL, and PHP (see Figure 20‑1). The L can be RHEL, CentOS Stream, Fedora, Debian, Ubuntu, and so on. My companion site for this book, https://www.linuxprobe.com, is deployed on LNMP and has been stable and fast.

Figure 20-1 Logos of software in the LNMP stack

Before building from source, prepare a compilation environment: C, C++, and Perl compilers and common development libraries. Configure repositories, install the Development Tools group, and then install the following packages (output omitted):


root@linuxprobe:~# dnf groupinstall -y "Development Tools"
root@linuxprobe:~# dnf install -y libxcrypt-compat libxml2-devel sqlite-devel libcurl-devel

If possible, increase the VM’s memory to speed up compilation. Because I’ll fetch multiple source archives for Nginx, MySQL, PHP, and WordPress, I bridge the NIC and let it obtain settings via DHCP (Figure 20‑2), then verify internet access:


root@linuxprobe:~# ping -c 4 www.linuxprobe.com
PING www.linuxprobe.com.w.kunlunno.com (202.97.231.16) 56(84) bytes of data.
64 bytes from www.linuxprobe.com (202.97.231.16): icmp_seq=1 ttl=55 time=27.5 ms
64 bytes from www.linuxprobe.com (202.97.231.16): icmp_seq=2 ttl=55 time=27.10 ms
64 bytes from www.linuxprobe.com (202.97.231.16): icmp_seq=3 ttl=55 time=27.4 ms
64 bytes from www.linuxprobe.com (202.97.231.16): icmp_seq=4 ttl=55 time=28.9 ms

--- www.linuxprobe.com.w.kunlunno.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 8ms
rtt min/avg/max/mdev = 27.354/27.913/28.864/0.593 ms

Figure 20-2 Set the NIC to obtain settings via DHCP

I’ve uploaded five source packages needed for LNMP plus a WordPress zip archive to the companion site. You can download them in Windows and transfer over SSH, or fetch them directly on the Linux server. Create a working directory at /lnmp to keep things organized:


root@linuxprobe:~# mkdir /lnmp
root@linuxprobe:~# cd /lnmp
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/rpcsvc-proto-1.4.4.tar.xz
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/pcre-8.45.tar.gz
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/nginx-1.27.4.tar.gz
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/mysql-8.0.18.tar.xz
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/php-8.1.32.tar.gz
root@linuxprobe:/lnmp# wget https://www.linuxprobe.com/Software/wordpress.zip
root@linuxprobe:/lnmp# ls
mysql-8.0.18.tar.xz pcre-8.45.tar.gz rpcsvc-proto-1.4.4.tar.xz
nginx-1.27.4.tar.gz php-8.1.32.tar.gz wordpress.zip

Let’s start with a small one. rpcsvc‑proto contains rpcsvc protocol files needed later by Nginx and MySQL builds. Remember the five canonical steps: extract, configure, make, make install, clean. The following omits the long build output—follow your terminal messages as you work.


root@linuxprobe:/lnmp# tar xvf rpcsvc-proto-1.4.4.tar.xz
root@linuxprobe:/lnmp# cd rpcsvc-proto-1.4.4
root@linuxprobe:/lnmp/rpcsvc-proto-1.4.4# ./configure
root@linuxprobe:/lnmp/rpcsvc-proto-1.4.4# make
root@linuxprobe:/lnmp/rpcsvc-proto-1.4.4# make install
root@linuxprobe:/lnmp/rpcsvc-proto-1.4.4# cd ..
root@linuxprobe:/lnmp#

PCRE is a Perl‑compatible regular expression library. I won’t build it standalone now—just extract it under /usr/local/src so Nginx’s build can find it:


root@linuxprobe:/lnmp# tar xzvf pcre-8.45.tar.gz
root@linuxprobe:/lnmp# mv pcre-8.45 /usr/local/src/

Because this chapter touches many components, switching directories is inevitable. I try to return to /lnmp after each step. Watch your working path to avoid “file not found” frustration.

20.2.1 Configure the Nginx service

Nginx is an outstanding lightweight web server widely used for dynamic sites. Originally developed for a Russian portal, it earned trust for its stability, rich features, low memory footprint, and strong concurrency. Major Chinese portals such as Sina, NetEase, and Tencent use Nginx.

Nginx’s stability stems from phased resource allocation, which lowers CPU and memory usage. The module ecosystem is broad—proxy, rewrite, FastCGI, SSL, virtual hosts, and more—and hot deployment supports 24×7 upgrades without downtime.

To be candid: while Nginx code quality is high and extensions are easy, documentation—especially some Chinese materials—can be uneven. Even so, its momentum has been strong; its future in the lightweight web server space looks bright.

Now I configure Nginx.

Step 1: Create a dedicated system account to run the web service. Assign different services to different system users to reduce risk. If an attacker compromises the web service, they should not gain broader privileges or even be able to log in via SSH. Create the account without a home directory and with /sbin/nologin as its shell:


root@linuxprobe:/lnmp# useradd nginx -M -s /sbin/nologin
root@linuxprobe:/lnmp# id nginx
uid=1001(nginx) gid=1001(nginx) groups=1001(nginx)

Step 2: Compile and install Nginx. Add useful flags to configure: --prefix to choose the install path; --with-http_ssl_module to enable HTTPS support; and --with-pcre to point at the PCRE source so it is built in one pass.


root@linuxprobe:/lnmp# tar xzvf nginx-1.27.4.tar.gz
root@linuxprobe:/lnmp# cd nginx-1.27.4/
root@linuxprobe:/lnmp/nginx-1.27.4# ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.45
root@linuxprobe:/lnmp/nginx-1.27.4# make
root@linuxprobe:/lnmp/nginx-1.27.4# make install
root@linuxprobe:/lnmp/nginx-1.27.4# cd ..

Step 3: Post‑install configuration. Because I used --prefix, configuration lives under /usr/local/nginx. Make three edits to /usr/local/nginx/conf/nginx.conf.

First, set the service user and group on line 2:


root@linuxprobe:/lnmp# vim /usr/local/nginx/conf/nginx.conf
1
2 user nginx nginx;

Second, add index.php to the index line so PHP homepages are recognized:


43 location / {
44 root html;
45 index index.php index.html index.htm;
46 }

Third, enable FastCGI handling for PHP files by uncommenting lines 65–71 and adjust the root path so SCRIPT_FILENAME resolves correctly to your web root (/usr/local/nginx/html):


63 # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
64 #
65 location ~ \.php$ {
66 root html;
67 fastcgi_pass 127.0.0.1:9000;
68 fastcgi_index index.php;
69 fastcgi_param SCRIPT_FILENAME /usr/local/nginx/html$fastcgi_script_name;
70 include fastcgi_params;
71 }

Step 4: Because I installed from source, systemctl won’t manage Nginx automatically; use Nginx’s own binary under /usr/local/nginx/sbin. To avoid typing full paths, add it to PATH in ~/.bash_profile and reload the profile:


root@linuxprobe:/lnmp# vim ~/.bash_profile
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/bin:/usr/local/nginx/sbin
export PATH
root@linuxprobe:/lnmp# source ~/.bash_profile

Load the library path and start Nginx:


root@linuxprobe:/lnmp# echo "/usr/local/lib" > /etc/ld.so.conf.d/local-lib.conf
root@linuxprobe:/lnmp# ldconfig
root@linuxprobe:/lnmp# nginx

Allow HTTP in the firewall:


root@linuxprobe:/lnmp# firewall-cmd --permanent --add-service=http
success
root@linuxprobe:/lnmp# firewall-cmd --reload
success

Open a browser to the server’s IP to see Nginx’s default page (Figure 20‑3).

Figure 20-3 Nginx default page

20.2.2 Configure the MySQL service

Chapter 18 covered the relationship between MySQL and MariaDB and praised MariaDB’s strengths. Even so, MySQL remains one of the most widely used relational database management systems in production, with a large market share and a long track record of stability and security. To further cement fundamentals, I revisit core MySQL setup here.

MYSQL

When installing from a repository, RPM scripts perform many configuration steps automatically. When installing from source, I must do that work myself. First, create a dedicated mysql system user with a nologin shell:


root@linuxprobe:/lnmp# useradd mysql -M -s /sbin/nologin

Step 1: Extract the MySQL archive, rename the directory, and move it under /usr/local. Note that .tar.xz files should be extracted without the z flag.


root@linuxprobe:/lnmp# tar xvf mysql-8.0.18.tar.xz
root@linuxprobe:/lnmp# mv mysql-8.0.18-linux-glibc2.12-x86_64 mysql
root@linuxprobe:/lnmp# mv mysql /usr/local

Step 2: In production there are two important directories: /usr/local/mysql for program files and /usr/local/mysql/data for databases. Create the data directory:


root@linuxprobe:/lnmp# cd /usr/local/mysql
root@linuxprobe:/usr/local/mysql# mkdir data

Step 3: Initialize MySQL, set ownership, and record the temporary root password printed at the end (for example, yr+IUmh9l,/i below).


root@linuxprobe:/usr/local/mysql# chown -R mysql:mysql /usr/local/mysql
root@linuxprobe:/usr/local/mysql# cd bin
root@linuxprobe:/usr/local/mysql/bin# ./mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data
2025-04-07T02:17:21.539094Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.18) initializing of server in progress as process 42031
2025-04-07T02:17:23.141934Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: yr+IUmh9l,/i

Step 4: Add /usr/local/mysql/bin to PATH like I did for Nginx:


root@linuxprobe:/usr/local/mysql/bin# cd ~
root@linuxprobe:~# vim ~/.bash_profile
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/bin:/usr/local/nginx/sbin:/usr/local/mysql/bin
export PATH
root@linuxprobe:~# source ~/.bash_profile

Because I installed manually, I write a systemd unit file so systemctl can manage MySQL. MySQL 8.0 requires libtinfo.so.5; on RHEL 10 the library is named libtinfo.so.6, so I create a symlink:


root@linuxprobe:~# ln -s /usr/lib64/libtinfo.so.6 /usr/lib64/libtinfo.so.5

Write and load the service unit:


root@linuxprobe:~# vim /usr/lib/systemd/system/mysqld.service
[Unit]
Description=MySQL 8.0 Server
After=network.target

[Service]
User=mysql
Group=mysql
ExecStart=/usr/local/mysql/bin/mysqld
LimitNOFILE = 5000

[Install]
WantedBy=multi-user.target
root@linuxprobe:~# systemctl daemon-reexec
root@linuxprobe:~# systemctl daemon-reload

Adjust SELinux context for the mysqld binary, then start and enable the service:


root@linuxprobe:~# semanage fcontext -a -t bin_t /usr/local/mysql/bin/mysqld
root@linuxprobe:~# restorecon -v /usr/local/mysql/bin/mysqld
Relabeled /usr/local/mysql/bin/mysqld from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:bin_t:s0
root@linuxprobe:~# systemctl start mysqld
root@linuxprobe:~# systemctl enable mysqld
Created symlink '/etc/systemd/system/multi-user.target.wants/mysqld.service' → '/usr/lib/systemd/system/mysqld.service'.

Step 5: Perform initial login and password change. Since MySQL 8.0, security is tighter: you cannot use the temporary password for management tasks until you change it. Use a strong password (20+ characters is recommended). Example:


root@linuxprobe:~# mysql -u root -p
Enter password: ← enter the temporary password above
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.18
...
mysql> alter user 'root'@'localhost' identified by '7U9cnbMNVkcWVQZNtX9q';
Query OK, 0 rows affected (0.01 sec)

For MySQL 8.0, also set the authentication plugin explicitly for compatibility:


mysql> use mysql;
Database changed
mysql> show tables;
+---------------------------+
| Tables_in_mysql |
+---------------------------+
| columns_priv |
| tables_priv |
| time_zone |
| time_zone_leap_second |
| time_zone_name |
| time_zone_transition |
| time_zone_transition_type |
| user |
| … output truncated … |
+---------------------------+

mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '7U9cnbMNVkcWVQZNtX9q';
Query OK, 0 rows affected (0.01 sec)

Create the database needed by WordPress (used in Section 20.3):


mysql> create database linuxcool;
Query OK, 1 row affected (0.00 sec)

mysql> exit
Bye

20.2.3 Configure the PHP service

PHP

PHP (Hypertext Preprocessor) is a general‑purpose, open‑source scripting language created in 1995. It borrows from C, Java, and Perl; it is free, fast, cross‑platform, and efficient—one of the most widely used languages for web development.

Installing PHP from source is straightforward; the tricky part is satisfying dependencies.

Step 1: Extract, configure, and install. Use --prefix to choose the install path, and enable MySQL‑related options needed by dynamic sites.


root@linuxprobe:~# cd /lnmp
root@linuxprobe:/lnmp# tar xvf php-8.1.32.tar.gz
root@linuxprobe:/lnmp# cd php-8.1.32
root@linuxprobe:/lnmp/php-8.1.32# ./configure --prefix=/usr/local/php --enable-fpm --with-mysqli --with-curl --with-pdo-mysql --with-pdo-sqlite --enable-mysqlnd --with-openssl --with-zlib

Build and install (this may take 10–20 minutes):


root@linuxprobe:/lnmp/php-8.1.32# make
root@linuxprobe:/lnmp/php-8.1.32# make install

Step 2: Copy configuration files. Copy php.ini into place, then prepare php-fpm’s main and pool configs:


root@linuxprobe:/lnmp/php-8.1.32# cp php.ini-development /usr/local/php/lib/php.ini
root@linuxprobe:/lnmp/php-8.1.32# cd /usr/local/php/etc/
root@linuxprobe:/usr/local/php/etc# mv php-fpm.conf.default php-fpm.conf
root@linuxprobe:/usr/local/php/etc# mv php-fpm.d/www.conf.default php-fpm.d/www.conf
root@linuxprobe:/usr/local/php/etc# cd ~

Step 3: Create a systemd unit for php-fpm and enable it at boot:


root@linuxprobe:~# vim /usr/lib/systemd/system/php-fpm.service
[Unit]
Description=The PHP 8.1.32 FastCGI Process Manager
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/php/sbin/php-fpm --nodaemonize --fpm-config /usr/local/php/etc/php-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target
root@linuxprobe:~# systemctl daemon-reexec
root@linuxprobe:~# systemctl daemon-reload

Step 4: Harden php.ini. Some functions are dangerous in shared web hosting and can be disabled. On line 323, append a list based on real‑world experience (adjust for your needs):


root@linuxprobe:~# vim /usr/local/php/lib/php.ini
320 ; This directive allows you to disable certain functions.
321 ; It receives a comma-delimited list of function names.
322 ; https://php.net/disable-functions
323 disable_functions = passthru,exec,system,chroot,chgrp,chown,shell_exec,proc _open,proc_get_status,popen,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server

Step 5: Start and enable php-fpm:


root@linuxprobe:~# systemctl start php-fpm
root@linuxprobe:~# systemctl enable php-fpm

20.3 Deploy a WordPress blog

To validate the LNMP environment, I deploy WordPress and test. If WordPress installs and runs successfully, the stack is sound. WordPress is written in PHP and runs on servers that support PHP and MySQL. With a rich ecosystem of plugins and themes, it is currently the most popular CMS. As of April 2025, 43.2% of the world’s top 10 million sites use WordPress; among CMS‑backed sites, WordPress holds a 62.7% share.

Clear Nginx’s default document root and copy the WordPress files into place:


root@linuxprobe:~# cd /lnmp
root@linuxprobe:/lnmp# rm -f /usr/local/nginx/html/*
root@linuxprobe:/lnmp# unzip wordpress.zip
root@linuxprobe:/lnmp# mv wordpress/* /usr/local/nginx/html

Ensure the web root is owned by nginx and is readable/writable by the service:


root@linuxprobe:/lnmp# chown -Rf nginx:nginx /usr/local/nginx/html
root@linuxprobe:/lnmp# chmod -Rf 777 /usr/local/nginx/html

Browse to the site to see the installer’s welcome screen (Figure 20‑4). It lists the information you’ll need.

Figure 20-4 WordPress installer welcome

Click “Let’s go!” and enter the database name you created, the username, and the new password. Use 127.0.0.1 for the database host rather than localhost (Figure 20‑5). Click Submit to confirm, then run the installation (Figure 20‑6).

Figure 20-5 Enter connection information

Figure 20-6 Confirm installing WordPress

After installation, set the site title, admin username, and password (Figure 20‑7), then click Install WordPress. A success screen appears (Figure 20‑8).

Figure 20-7 Site title and admin account

Figure 20-8 Installation complete

Log in with the credentials you just set (Figure 20‑9) to reach the admin dashboard (Figure 20‑10). Explore and see what kinds of sites you can build with the most popular CMS on the web.

Figure 20-9 Log in with username and password

Figure 20-10 WordPress admin dashboard

20.4 Buying a server for your site

Public websites are composed of a domain name, site code, and a server host. The server stores your site and serves pages to users. As I wrap up, here are some practical tips for choosing a server—drawn from years of running sites.

Virtual hosting: A provider allocates a slice of disk space on a shared server for your site and data. This is low‑cost and nearly maintenance‑free beyond your site itself—ideal for small sites.

VPS (Virtual Private Server): Virtualization (OpenVZ, Xen, KVM) carves a physical server into multiple virtual machines, each with its own IP and OS. CPU, memory, disk, processes, and configuration are isolated. You manage the OS yourself and need some admin skills. Suitable for small sites.

ECS (Elastic Compute Service, commonly called cloud servers): ECS integrates compute, storage, and networking with horizontal scaling. Similar in use to a VPS, but built on clustered hosts with mirrored images, improving safety and stability. ECS adds flexibility and pay‑as‑you‑go pricing—good for all sizes of sites.

Dedicated server: Hardware exclusively for one customer, either rented or colocated. With rental, you specify the hardware and pay by month/quarter/year; the provider maintains the hardware while you install and manage the software. This reduces upfront capex and suits mid‑to‑large sites. With colocation, you purchase the hardware and place it at the provider’s facility (paying a management fee). You get full hardware control but must handle maintenance and repairs—again suiting mid‑to‑large sites.

A final reminder: check vendor reputation before you buy. Some providers limit features, inject ads, or hide fees. Do your homework and avoid traps.

Review Questions

  1. What is the biggest advantage and disadvantage of installing from source?
    Answer: Advantage—excellent portability and potentially better performance optimized for the local system; disadvantage—installation and lifecycle management are more complex and time‑consuming.

  2. Why extract pcre under /usr/local/src instead of building it immediately?
    Answer: Nginx’s build can consume that source directly via --with-pcre, compiling it in one pass.

  3. Why do I add index.php to Nginx’s index directive?
    Answer: So that requests to a directory automatically serve PHP homepages.

  4. What does the FastCGI SCRIPT_FILENAME parameter represent?
    Answer: The script path that maps the requested URL to a PHP file (here, /usr/local/nginx/html$fastcgi_script_name).

  5. Why create dedicated system users for nginx and mysql with /sbin/nologin?
    Answer: To isolate privileges and reduce risk; these accounts cannot be used for interactive logins.

  6. After running mysqld --initialize, where do I find the first-time password?
    Answer: It is printed at the end of the initialization output for root@localhost.

  7. In MySQL 8.0, why set mysql_native_password?
    Answer: For compatibility with some clients and tooling that expect that plugin.

  8. Why use 127.0.0.1 instead of localhost for WordPress’s DB host?
    Answer: It forces a TCP connection rather than a Unix socket, avoiding mismatches in socket paths.

  9. Why is php.ini hardening (disable_functions) recommended?
    Answer: It limits dangerous features in shared hosting scenarios, reducing exploit risk.

  10. What is the main trade‑off when choosing between VPS/ECS and a dedicated server?
    Answer: VPS/ECS offers flexibility and lower maintenance; dedicated gives full control and potential performance at the cost of higher responsibility and management.