Getting a WordPress Site Live Quickly with a Minimal LEMP Stack

Written by

in

Getting a WordPress Site Live Quickly with a Minimal LEMP Stack

Introduction

The larger Geek.click hosting project is being built as a proper segmented hosting environment using OPNsense, VLANs, reverse proxies and multiple hosting platforms.

That takes time.

On this occasion, however, I needed to get a single WordPress site online immediately. There was no point delaying the site while building the complete architecture that would eventually host it.

The decision was therefore to build a small, temporary LEMP server using the existing Proxmox and OPNsense environment.

The intention was simple:

Get one WordPress site online, using as much of the eventual LEMP stack as possible, without redesigning the entire infrastructure around it.

This setup is temporary. Once the main Geek.click hosting project is completed, the site will eventually be migrated into the proper architecture and this temporary installation will be removed.

The Temporary Architecture

The existing server already had:

  • Proxmox
  • OPNsense
  • A public IPv4 address on Proxmox
  • A separate public IPv4 address assigned to OPNsense WAN
  • An internal 192.168.1.0/24 network

The public Proxmox address was already in use, so it was not possible to simply give the same address to another virtual machine.

Instead, the new WordPress server was placed behind OPNsense.

The resulting temporary layout was:

Internet
   |
   v
Public IP
   |
   v
OPNsense
   |
   | NAT :80 / :443
   |
   v
Debian LXC
192.168.1.28
   |
   +-- Nginx
   +-- PHP-FPM
   +-- MariaDB
   +-- WordPress

The actual addresses have been replaced here with generic examples.

Why an LXC?

There was no requirement for a complicated deployment platform.

A small Debian LXC was sufficient and was quick to create.

The container was configured with approximately:

  • Debian 13
  • 2 CPU cores
  • 2 GB RAM
  • 16 GB storage
  • 512 MB swap
  • Internal IP address
  • Gateway pointing to OPNsense

The container was created as an unprivileged LXC.

The important point was that the container did not need a public IP of its own. OPNsense would handle the public-facing NAT.

Installing the LEMP Stack

Once the Debian container was running, the system was updated:

apt update && apt upgrade -y

The required software was then installed:

apt install -y nginx mariadb-server php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl unzip curl

This installed the basic LEMP components.

Nginx

Nginx is the web server.

It receives HTTP/HTTPS requests and serves the WordPress site.

MariaDB

MariaDB is the database server.

WordPress stores its posts, pages, users, settings and other data in the database.

PHP-FPM

PHP-FPM executes PHP code for Nginx.

WordPress is written in PHP, so Nginx needs PHP-FPM to actually execute the WordPress application.

PHP Extensions

Several PHP extensions were installed for WordPress functionality, including database connectivity, image handling, XML, ZIP support and HTTP requests.

OPcache

PHP’s OPcache was also installed as part of the PHP package setup.

It caches compiled PHP code and can improve PHP performance.

Checking the Installation

PHP was checked with:

php -v

The installation was running PHP 8.4.

Nginx and MariaDB were also checked:

systemctl status nginx mariadb --no-pager

Both services were running successfully.

At this point the basic server stack was operational.

Creating the WordPress Database

A dedicated MariaDB database and user were created for WordPress.

The database was created with UTF-8 support:

CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

A dedicated database user was then created:

CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD';

The user was granted access only to the WordPress database:

GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;

The password was subsequently changed after initially entering an incorrect value.

The important lesson here was simple: test the credentials before getting too far into the installation.

The database connection was tested directly:

mysql -u wpuser -p wordpress

The login worked successfully.

Connecting Nginx to PHP-FPM

The first PHP test exposed a configuration issue.

A simple PHP information page was created:

echo '<?php phpinfo();' > /var/www/html/info.php

The test was then performed:

curl http://127.0.0.1/info.php

Instead of executing PHP, Nginx returned the PHP source code:

<?php phpinfo();

This immediately showed that PHP-FPM was installed but Nginx was not yet passing PHP requests to it.

The PHP-FPM socket was confirmed:

ls /run/php/

which showed:

php-fpm.sock
php8.4-fpm.pid
php8.4-fpm.sock

The Nginx default site configuration was then updated.

The important part was:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

The Nginx configuration was tested:

nginx -t

and then reloaded:

systemctl reload nginx

The PHP test was performed again.

This time PHP was actually executed and the PHP information page was returned.

The temporary test file was then removed:

rm /var/www/html/info.php

Installing WordPress

WordPress was downloaded directly into the Nginx web root.

cd /var/www/html

Then:

curl -O https://wordpress.org/latest.tar.gz

The archive was extracted:

tar -xzf latest.tar.gz --strip-components=1

The archive was removed:

rm latest.tar.gz

Finally, ownership was changed:

chown -R www-data:www-data /var/www/html

This allows the web server/PHP process to work with the WordPress files.

Testing WordPress Before Going Public

Before configuring DNS or opening the site to the Internet, WordPress was tested locally.

curl -I http://127.0.0.1/

WordPress returned:

HTTP/1.1 302 Found

and redirected to:

/wp-admin/setup-config.php

That was exactly what should happen at this stage.

The WordPress installer was therefore functioning.

OPNsense NAT

The WordPress container had an internal address and was not directly exposed to the Internet.

OPNsense was used to forward the public traffic.

A destination NAT rule was created for HTTP:

WAN :80
    |
    v
WordPress LXC :80

A second rule was created for HTTPS:

WAN :443
    |
    v
WordPress LXC :443

The associated firewall rules were created automatically using the Pass option.

One small lesson here: creating the NAT rule is not necessarily the final step. The changes also need to be applied in OPNsense.

Initially the HTTP request appeared to be reaching OPNsense rather than the WordPress container because the new NAT configuration had not yet been applied.

Once applied, the traffic reached the container correctly.

DNS

The domain’s DNS was configured to point to the OPNsense public address.

The resulting flow was:

wowforeverhub.com
        |
        v
Public IP
        |
        v
OPNsense
        |
        v
192.168.1.28

Cloudflare was initially set to DNS-only while testing the direct connection.

DNS was verified from the server:

dig +short A example.com

The correct public IPv4 address was returned.

Running the WordPress Installer

With the networking working, the WordPress setup page became accessible.

The database settings were:

Database Name:     wordpress
Username:          wpuser
Password:          [the password created earlier]
Database Host:     localhost
Table Prefix:      wp_

WordPress successfully connected to MariaDB and completed its installation.

At this point the site was already functioning over HTTP.

Adding HTTPS

The next step was HTTPS.

Certbot and its Nginx integration were installed:

apt install -y certbot python3-certbot-nginx

Certbot was then used to obtain a Let’s Encrypt certificate:

certbot --nginx -d example.com -d www.example.com

Certbot successfully obtained a certificate for the domain and configured Nginx to use it.

The resulting Nginx configuration included:

listen 443 ssl;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

The certificate was verified with:

certbot certificates

The certificate was valid and had an expiry date approximately 90 days in the future, as expected for a Let’s Encrypt certificate.

A Strange HTTPS Test

There was one interesting final problem.

Testing the public domain from inside the WordPress container produced:

SSL certificate problem: self-signed certificate

At first this looked like a certificate problem.

However, the browser showed the site as completely secure and displayed the correct Let’s Encrypt certificate.

A direct HTTPS test against the container’s internal address confirmed that Nginx was serving the correct site:

curl -I https://192.168.1.28 -k

which returned:

HTTP/1.1 200 OK
Server: nginx

The problem was therefore not the WordPress server or its certificate.

The container was attempting to reach its own public address from inside the LAN, passing through the OPNsense routing/NAT path. OPNsense was responding with its own certificate instead of the certificate being served by Nginx.

The important test was the real one:

Open the website from the Internet.

The browser showed a valid HTTPS connection.

Therefore nothing needed to be changed.

Final Result

The temporary hosting environment ended up looking like this:

                         INTERNET
                            |
                            v
                       Cloudflare DNS
                            |
                            v
                       Public IPv4
                            |
                            v
                        OPNsense
                     /             \
                NAT :80          NAT :443
                   |                 |
                   +--------+--------+
                            |
                            v
                    Debian 13 LXC
                     192.168.1.28
                            |
             +--------------+--------------+
             |              |              |
           Nginx         PHP-FPM        MariaDB
             |              |
             +------ WordPress

The final checks confirmed:

  • Debian LXC working
  • Network connectivity working
  • Nginx working
  • PHP-FPM working
  • MariaDB working
  • WordPress working
  • OPNsense NAT working
  • DNS working
  • Let’s Encrypt certificate working
  • Public HTTPS working

The WordPress site was therefore live.

What I Deliberately Didn’t Build Yet

This was intentionally not the final Geek.click hosting architecture.

I did not stop to implement:

  • VLAN separation
  • Nginx Proxy Manager
  • a dedicated management network
  • a dedicated website network
  • WireGuard management access
  • WAF configuration
  • IDS/IPS
  • elaborate firewall policies
  • container orchestration
  • production monitoring
  • the final backup architecture
  • multiple hosting platforms

Those are all part of the larger project.

The purpose of this exercise was simply to get one WordPress site online quickly while still using the underlying technologies that I actually want to understand.

What This Will Become Later

The temporary server is useful because almost everything here can eventually be reused conceptually.

The final Geek.click hosting architecture will be more structured:

Internet
    |
    v
OPNsense
    |
    v
Management / Proxy / Website VLANs
    |
    v
Nginx Proxy Manager
    |
    +---- Manual LEMP
    |
    +---- CloudPanel
    |
    +---- Coolify

The temporary WordPress installation can then be migrated into that architecture when the larger project is complete.

For now, however, the important objective was achieved:

one WordPress site, running on a real LEMP stack, publicly accessible over HTTPS — without waiting for the entire hosting laboratory to be built.

Comments

Leave a Reply

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