LEMP Stack

Table of contents

  1. Overview
  2. Install System Dependencies
  3. Install Nginx
  4. Install MySQL/MariaDB
  5. Install PHP 8.4
  6. Configure Nginx and PHP-FPM
  7. Install phpMyAdmin
  8. Install Composer
  9. Install Node.js & NPM
  10. Project Setup
  11. Troubleshooting
  12. References


1. Overview Table of Contents

This guide walks through setting up a LEMP stack (Linux, Nginx, MySQL/MariaDB, PHP) on Ubuntu (22.04 LTS), Debian (LMDE), and RHEL (Rocky Linux) for deploying PHP applications, including the Chappy.php framework.

Requirements

  • Nginx
  • PHP 8.3+
  • MySQL or MariaDB
  • Composer
  • Node.js & NPM
  • Git (for cloning the repository)


2. Install System Dependencies Table of Contents

First, update your system and install essential dependencies:

Ubuntu & Debian

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git unzip software-properties-common net-tools

Rocky Linux (RHEL-based)

sudo dnf install -y epel-release
sudo dnf update -y
sudo dnf install -y curl wget git unzip net-tools


3. Install Nginx Table of Contents

A. Install Nginx and enable it to start on boot:

Ubuntu and Debian

sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx

Rocky Linux (RHEL-based)

sudo dnf install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx


B. Verify Nginx is Running

To confirm that Nginx is running:

systemctl status nginx

The default page for Nginx should be accessible at:

http://localhost


4. Install MySQL/MariaDB Table of Contents

This section will guide you through installing MySQL or MariaDB, configuring it securely, and verifying the installation.

A. Install MySQL or MariaDB

Depending on your Linux distribution, install either MySQL or MariaDB using the following instructions.

Ubuntu

To install MySQL:

sudo apt install -y mysql-server
sudo systemctl enable mysql
sudo systemctl start mysql


Ubuntu and Debian

To install MariaDB:

sudo apt install -y mariadb-server
sudo systemctl enable mariadb
sudo systemctl start mariadb


Rocky Linux (RHEL-based)

To install MySQL:

sudo dnf install -y https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm
sudo dnf install -y mysql-server
sudo systemctl enable mysqld
sudo systemctl start mysqld

To install MariaDB:

sudo dnf install -y mariadb-server
sudo systemctl enable mariadb
sudo systemctl start mariadb


Verify MySQL/MariaDB Installation

Run the following command to check the installed version:

mysql -V


B. Secure MySQL/MariaDB Using mysql_secure_installation

Run the mysql_secure_installation script to secure your database by setting a root password and removing default insecure settings.

Ubuntu/Debian (MySQL 8+)

sudo mysql_secure_installation
  • MySQL 8+ on Ubuntu/Debian defaults to auth_socket authentication, meaning the root user does NOT need a password to log in locally.
  • The root password setup step may be skipped during the process.


Rocky Linux (RHEL-based)

sudo mysql_secure_installation
  • MySQL on Rocky Linux requires setting a root password during installation.

C. Steps for Running mysql_secure_installation

The script will ask a series of security questions. Here’s a breakdown of the common prompts and how to respond:

1. VALIDATE PASSWORD COMPONENT (Password Policy)

You will see this message:

VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD component?

Press y|Y for Yes, any other key for No:
  • ✅ MySQL: Type n unless you want strict password rules. Choosing y might cause errors (e.g., ERROR 1819 (HY000)) during tools like phpMyAdmin setup.
  • ✅ MariaDB: This step may not appear. If it does, it’s safe to disable it (n) for local or development use.
  • If you choose y, you must select a password strength level:
0 = LOW (minimum 8 characters)
1 = MEDIUM (includes numbers, mixed case, special characters)
2 = STRONG (must contain dictionary words + mixed case, numbers, and special characters)

Choose 1 for a good balance.


2. Set or Change the Root Password

Ubuntu/Debian

  • If auth_socket is enabled, this step will be skipped.
  • If prompted:
    Would you like to set up a root password? [Y/n]
    

Rocky Linux (RHEL-based)

  • You must set a root password.
    Would you like to set up a root password? [Y/n]
    
  • MySQL (Ubuntu/Debian): Skipped if auth_socket is enabled.
  • MySQL (RHEL-based): You must set a password.
  • ✅ MariaDB (All distros): You’ll be asked even if a password is already set. Choose n if you already have it protected with a password or socket.


3. Switch to unix_socket Authentication (MariaDB Only)

Switch to unix_socket authentication [Y/n]
  • ✅ MariaDB on Debian/Ubuntu: Choose n if you want to use password authentication (especially for phpMyAdmin compatibility).
  • Choose y only if you’re confident with CLI-only login and not using GUI tools like phpMyAdmin.


4. Remove Anonymous Users

  • You’ll see this prompt:
    Remove anonymous users? (Press y|Y for Yes, any other key for No) :
    
  • Type Y and press Enter to improve security.


5. Disable Remote Root Login

  • You’ll see this prompt:
    Disallow root login remotely? (Press y|Y for Yes, any other key for No) :
    
  • Type Y and press Enter to prevent unauthorized remote access.


6. Remove the Test Database

  • You’ll see:
    Remove test database and access to it? (Press y|Y for Yes, any other key for No) :
    
  • Type Y and press Enter.


7. Reload Privilege Tables

  • Finally, MySQL will ask:
    Reload privilege tables now? (Press y|Y for Yes, any other key for No) :
    
  • Type Y and press Enter to apply all changes.


8. Secure Installation Complete

  • You should see a message like:
    All done! If you've completed all of the above steps, your MySQL installation should now be secure.
    


D. Verify MySQL is Secure

After running mysql_secure_installation, you can verify your settings by logging in:

sudo mysql -u root -p
  • If prompted, enter the root password you set earlier.


Run this SQL command to authentication settings:

SELECT user, host FROM mysql.user;
  • Ensure that root@localhost exists and that anonymous users were removed.


E. Changing MySQL Authentication (Ubuntu/Debian)

The step to set root password is skipped in Ubuntu and Debian.

Skipping password set for root as authentication with auth_socket is used by default.
If you would like to use password authentication instead, this can be done with the "ALTER_USER" command.
See https://dev.mysql.com/doc/refman/8.0/en/alter-user.html#alter-user-password-management for more information.


F. How to Switch MySQL Root to Password Authentication (Ubuntu & Debian)

If you want to use a password for the root user instead of auth_socket, follow these steps:

1. Log into MySQL as Root

Since auth_socket is enabled, use sudo to access MySQL without a password:

sudo mysql


2. Check Current Authentication Method

Run this SQL command:

SELECT user, host, plugin FROM mysql.user;

You should see root@localhost using auth_socket, like this:

+------+-----------+-------------+
| user | host     | plugin      |
+------+-----------+-------------+
| root | localhost | auth_socket |
+------+-----------+-------------+


3. Change Root to Use Password Authentication

To switch from auth_socket to mysql_native_password, run:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your-secure-password';
FLUSH PRIVILEGES;


4. Verify the Change

Run the authentication check again:

SELECT user, host, plugin FROM mysql.user;

It should now show mysql_native_password instead of auth_socket.


5. Exit and Test

Exit MySQL:

EXIT;

Now try logging in with your new password:

mysql -u root -p


6. Final Notes

  • For production servers, always use a strong root password and disable remote root login.
  • If you forget your MySQL root password, you’ll need to reset it manually via mysqld_safe mode.


5. Install PHP 8.4 Table of Contents

A. Ubuntu

sudo add-apt-repository ppa:ondrej/php -y
sudo apt update && sudo apt upgrade -y
sudo apt install -y php8.4 php8.4-cli php8.4-fpm php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-mysql php8.4-sqlite3 sqlite3 php8.4-bcmath

Enable and start PHP-FPM:

sudo systemctl enable php8.4-fpm
sudo systemctl start php8.4-fpm


B. Debian

1. Add the SURY repository (trusted PHP repo for Debian)

sudo apt install -y lsb-release apt-transport-https ca-certificates wget gnupg2

Then import the GPG key:

wget -qO - https://packages.sury.org/php/apt.gpg | sudo tee /etc/apt/trusted.gpg.d/php.gpg >/dev/null

Now add the repo to your sources list:

echo "deb https://packages.sury.org/php/ bookworm main" | sudo tee /etc/apt/sources.list.d/php.list


2. Update and install PHP 8.4

sudo apt update && sudo apt upgrade -y
sudo apt install -y php8.4 php8.4-cli php8.4-fpm php8.4-mysql php8.4-curl php8.4-zip php8.4-mbstring php8.4-xml php8.4-bcmath php8.4-soap php8.4-intl php8.4-readline php8.4-sqlite3 sqlite3

Enable and start PHP-FPM:

sudo systemctl enable php8.4-fpm
sudo systemctl start php8.4-fpm


C. Rocky Linux (RHEL-based)

sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module list php                    # Optional: list available versions
sudo dnf module enable php:remi-8.4 -y      # Enable PHP 8.4 from Remi
sudo dnf install -y php php-cli php-fpm php-mbstring php-xml php-curl php-zip php-mysqlnd php-bcmath php-json php-gd php-opcache php-intl php-pear php-soap

Enable and start PHP-FPM:

sudo systemctl enable php-fpm
sudo systemctl start php-fpm


D. Verify installation:

php -v


6. Configure Nginx and PHP-FPM Table of Contents

A. Configure Nginx Server Block

Create your project and replace my-app with the name of your project. Then move to location described below:

cd ~/
composer create-project chappy-php/chappy-php my-app
sudo mv my-app/ /var/www/
cd /var/www/my-app
<br>

### B. Set proper permissions:
**Ubuntu**
```sh
sudo chown -R your-username:www-data /var/www/my-app
sudo chmod -R 755 /var/www/my-app

Rocky Linux (RHEL-based)

sudo chown -R your-username:nginx /var/www/my-app
sudo chmod -R 755 /var/www/my-app


C. Create a new configuration file:

Ubuntu & Debian

sudo vi /etc/nginx/sites-available/my-app

Paste the following content while making sure correct php version is set (replace myapp.local and my_ip_address with information relevant for your case):

server {
    listen 80;
    server_name myapp.local my_ip_address;
    root /var/www/my-app;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    charset utf-8;

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

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

    error_page 404 /index.php;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Enable the new site and disable default on Ubuntu/Debian:

sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/
sudo unlink /etc/nginx/sites-enabled/default


Rocky Linux (RHEL-based)

sudo vi /etc/nginx/conf.d/my-app.conf

Paste the following content while making sure correct php version is set (replace myapp.local and my_ip_address with information relevant for your case):

server {
    listen 80;
    server_name myapp.local my_ip_address;
    root /var/www/my-app;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    charset utf-8;

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

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

    error_page 404 /index.php;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Allow Nginx to Write and Connect:

sudo setsebool -P httpd_unified 1
sudo setsebool -P httpd_read_user_content 1
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_enable_homedirs 1


D. Test PHP

Then test the configuration and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Create a test file:

echo "<?php phpinfo(); ?>" | sudo tee /var/www/my-app/info.php

Open in a browser:

http://localhost/info.php

Remove the file after testing:

sudo rm /var/www/my-app/info.php


E. Configure Upload Size (for Profile Image Support):

Ubuntu and Debian

sudo vi /etc/php/8.4/fpm/php.ini

Rocky Linux (RHEL-based)

sudo vi /etc/php.ini

Then modify the following settings:

Setting What it controls Safe recommended value
upload_max_filesize Max size of a single uploaded file 5M (or 10M if high-res image uploads)
post_max_size Max size of total POST body (form fields + files) 8M (or 15M if upload_max_filesize is 10M)
max_execution_time Max script run time (seconds) 30 to 60
memory_limit Max memory a script can use 128M (or 256M for image-heavy apps)

These value should be set depending on what type of files being uploaded. Files such as videos should be much higher. post_max_size should be greater than upload_max_filesize. Otherwise, you will get a corrupted token error instead.


Then restart PHP-FPM:

Ubuntu and Debian

sudo systemctl restart php8.4-fpm

Rocky Linux (RHEL-based)

sudo systemctl restart php-fpm



7. Install phpMyAdmin Table of Contents

phpMyAdmin provides a web interface to manage MySQL or MariaDB databases.

A. Install phpMyAdmin

Ubuntu & Debian

sudo apt install -y phpmyadmin

When prompted:

  • Do not select a web server (Nginx is not listed).
  • Choose Yes to configure dbconfig-common and create the phpMyAdmin database.
  • Set a phpMyAdmin password or leave it blank to auto-generate.


Rocky Linux (RHEL-based)

sudo dnf install -y phpmyadmin


B. Configure Nginx to Serve phpMyAdmin

Ubuntu & Debian Symlink phpMyAdmin into your Nginx-accessible root path:

sudo ln -s /usr/share/phpmyadmin /var/www/phpmyadmin

Then edit your Nginx config:

sudo vi /etc/nginx/sites-enabled/my-app

Add the following inside the server {} block:

location /phpmyadmin {
    root /var/www;
    index index.php index.html index.htm;

    location ~ ^/phpmyadmin/(.+\.php)$ {
        try_files $uri =404;
        root /var/www;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* ^/phpmyadmin/(.+\.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ {
        root /var/www;
    }
}
  • Note: If you get a 202 Bad Gateway error check the version of PHP in your file. If you’re using a custom root path like /var/www/my-app, adjust root accordingly.


Rocky Linux (RHEL-based) Symlink phpMyAdmin into your Nginx-accessible root path:

sudo ln -s /usr/share/phpMyAdmin /var/www/phpmyadmin

Then edit your Nginx config:

sudo vi /etc/nginx/conf.d/my-app.conf

Add the following inside the server {} block:

location /phpmyadmin {
    root /var/www;
    index index.php index.html index.htm;

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

    location ~* ^/phpmyadmin/(.+\.(jpg|jpeg|gif|css|png|js|ico|html|xml|txt))$ {
        root /var/www;
    }
}


Reload Nginx to apply the config:

sudo nginx -t
sudo systemctl reload nginx


C. Verify phpMyAdmin Installation

Open your browser and visit:

http://localhost/phpmyadmin

Log in using:

  • Username: root
  • Password: (set during MySQL/MariaDB setup) ⚠️ If you’re using auth_socket, see Section 4F to switch to mysql_native_password.

D. Setup Your Database

  • In the left panel click on the New link.
  • In the main panel under Create Database enter the name for your database. This will be the database you will set to DB_DATABASE in your .env file.
  • Click create.


8. Install Composer Table of Contents

Composer is required to manage PHP dependencies.

A. Download and Install Composer

cd ~/
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer


B. Verify Installation

composer -v


9. Install Node.js & NPM Table of Contents

Use NodeSource to install the latest stable Node.js version.

A. Add Node.js Repository

Ubuntu and Debian

sudo apt install -y ca-certificates     # On minimal OS installs
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -

Rocky Linux (RHEL-based)

# ca-certificates on minimal OS installs
rpm -q ca-certificates || sudo dnf install -y ca-certificates
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -


B. Install Node.js & NPM

Ubuntu

sudo apt install -y nodejs

Rocky Linux (RHEL-based)

sudo dnf install -y nodejs


C. Verify Installation

node -v
npm -v


10. Project Setup Table of Contents

A. Navigate to your user’s root directory and install dependencies:

cd /var/www/my-app/
composer run install-project


B. Project Configuration

Open your preferred IDE (We use VSCode) and edit the .env file:

  • Set APP_DOMAIN TO /.
  • Update the database section:
    # Set to mysql or mariadb for production
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    # Set to your database name for production
    DB_DATABASE=your_db_name
    DB_USER=root
    DB_PASSWORD=your_password
    

Use a user other than root on a production environment


C. Update /etc/hosts (For Custom Domain)

If you want to access your site using http://my-app.local, you can edit your /etc/hosts file:

sudo vi /etc/hosts

Example configuration:

127.0.0.1       localhost my-app.local
127.0.1.1       ubuntu-vm
your_ip_addr    my-app.local

Now, http://my-app.local will work as expected.


D. Enable the Site:

Restart Nginx After Modifying Server Block or /etc/hosts:

sudo systemctl restart nginx

See Section D in Troubleshooting if you are having issues with Rocky Linux (RHEL)


Rocky Linux (RHEL-based)

1. Fix SELinux Contexts for Nginx

Allow Nginx to read and serve content from your project directory:

sudo chcon -Rt httpd_sys_content_t /var/www/my-app

Also apply recursively to any .htaccess, uploads, or views:

sudo restorecon -Rv /var/www/my-app

2. Set the Correct SELinux Context for Writable Log Files and to Storage:

Step 1: Apply the Right Context:

sudo chcon -R -t httpd_sys_rw_content_t /var/www/my-app/storage

Step 2: Make it Persistent (Survives Reboots):

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/my-app/storage(/.*)?"
sudo restorecon -Rv /var/www/my-app/storage

📁 If you’re using other writable paths, repeat these steps for those as well.

3. Add Firewalld Rules for Nginx

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Now restart Nginx:

sudo systemctl restart nginx


F. Final Steps

Set permissions and ownership for storage directory (This will enable writing to logs and uploads): Ubuntu & Debian

sudo chown -R chadchapman:www-data storage/
sudo chmod -R 775 storage/

Rocky Linux (RHEL-based)

sudo chown -R nginx:nginx storage/
sudo chmod -R 777 storage/


Run migrations:

php console migrate

Your project should now be accessible at:

http://<your_ip_address>


11. Troubleshooting Table of Contents

A. Common Issues:

  • ERROR 1819 (HY000) during phpMyAdmin installation → Your password does not meet MySQL’s policy. Disable VALIDATE PASSWORD or use a strong password.
  • mysql_secure_installation skips root password setup on Ubuntu/Debian → Run:
    sudo mysql
    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your-password';
    FLUSH PRIVILEGES;
    


B. SELinux Permissions for phpMyAdmin (Rocky Linux)

Since phpMyAdmin is installed via dnf on Rocky Linux, SELinux might block it from reading /var/www/phpmyadmin.

🔹 Fix: If users get 403 Forbidden on phpMyAdmin, they need to run:

sudo chcon -R -t httpd_sys_content_t /usr/share/phpmyadmin
sudo semanage fcontext -a -t httpd_sys_content_t "/usr/share/phpmyadmin(/.*)?"
sudo restorecon -Rv /usr/share/phpmyadmin


C. MySQL 8 Authentication in Rocky Linux

If mysql_secure_installation doesn’t ask for a password, users may need to set one manually:

sudo mysql
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your-secure-password';
FLUSH PRIVILEGES;
EXIT;


12. References Table of Contents

A. How To Install Linux, Nginx, MySQL, PHP (LEMP stack) on Ubuntu

B. How To Install and Configure Laravel with Nginx on Ubuntu 22.04 (LEMP)

C. How to Install PHP 8.3 on Ubuntu 22.04

D. How To Install and Secure phpMyAdmin with Nginx on an Ubuntu 20.04 Server