LEMP Stack
Table of contents
- Overview
- Install System Dependencies
- Install Nginx
- Install MySQL/MariaDB
- Install PHP
- Configure Nginx and PHP-FPM
- Install phpMyAdmin
- Install Composer
- Install Node.js & NPM
- Project Setup
- Troubleshooting
- References
1. Overview Table of Contents
This guide walks through setting up a LEMP stack (Linux, Nginx, MySQL/MariaDB, PHP) for deploying PHP applications, including the Chappy.php framework.
Supported operating systems
| Family | Versions | Package manager |
|---|---|---|
| Ubuntu | 26.04 LTS, 24.04 LTS | apt |
| Debian | Debian 13 “trixie” / LMDE 7 “Gigi” | apt |
| RHEL | Rocky Linux 10.x, 9.x | dnf |
- Commands marked Ubuntu & Debian apply to every
aptsystem above, and commands marked Rocky Linux apply to Rocky 9 and 10 unless noted. - Rocky Linux 10 requires an x86-64-v3 CPU (roughly Intel Haswell / AMD Excavator or newer). On a VM, set the CPU type to
hostor another v3-capable model.
Requirements
- Nginx
- PHP 8.3+ with PHP-FPM
- 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 ca-certificates net-tools
Rocky Linux (RHEL-based)
sudo dnf install -y epel-release
sudo dnf config-manager --set-enabled crb # Some EPEL packages depend on CRB
sudo dnf update -y
sudo dnf install -y curl wget git unzip net-tools policycoreutils-python-utils
policycoreutils-python-utils provides semanage, which is used for the SELinux steps in Section 10.
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 --now nginx
Rocky Linux (RHEL-based)
sudo dnf install -y nginx
sudo systemctl enable --now nginx
B. Open the firewall:
Ubuntu and Debian (only if ufw is enabled)
sudo ufw allow "Nginx Full"
Rocky Linux (RHEL-based) (firewalld is on by default)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
C. 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
Install one database server, not both. Chappy.php works with either.
| OS | MariaDB | MySQL |
|---|---|---|
| Ubuntu 24.04 / 26.04 | ✅ Default repos | ✅ Default repos |
| Debian 13 / LMDE 7 | ✅ Default repos (recommended) | ⚠️ Not in Debian repos. Requires the MySQL APT repository |
| Rocky Linux 9 / 10 | ✅ Default repos | ✅ Default repos (AppStream) |
If you don’t have a reason to pick MySQL, use MariaDB. It’s available everywhere in this guide without extra repositories.
A. Install the Database Server
MariaDB: Ubuntu & Debian
sudo apt install -y mariadb-server
sudo systemctl enable --now mariadb
MariaDB: Rocky Linux
sudo dnf install -y mariadb-server
sudo systemctl enable --now mariadb
MySQL: Ubuntu
sudo apt install -y mysql-server
sudo systemctl enable --now mysql
MySQL: Rocky Linux
sudo dnf install -y mysql-server
sudo systemctl enable --now mysqld
Verify the installation:
mariadb -V # MariaDB
mysql -V # MySQL
B. Secure the Installation
Run the security script for your database:
sudo mariadb-secure-installation # MariaDB
sudo mysql_secure_installation # MySQL
The prompts differ slightly between the two. Answer them as follows:
| Prompt | MariaDB | MySQL (Ubuntu) | MySQL (Rocky) |
|---|---|---|---|
| Enter current password for root | Press Enter | — | — |
| VALIDATE PASSWORD COMPONENT | — | n (see note) |
n (see note) |
| Switch to unix_socket authentication | n (root already uses it) |
— | — |
| Set / change root password | n |
Skipped (uses auth_socket) |
Required: set one |
| Remove anonymous users | Y |
Y |
Y |
| Disallow root login remotely | Y |
Y |
Y |
| Remove test database | Y |
Y |
Y |
| Reload privilege tables | Y |
Y |
Y |
Notes
- VALIDATE PASSWORD COMPONENT: Choosing
yenforces password strength rules and can causeERROR 1819 (HY000)during the phpMyAdmin install. If you do enable it, level1(MEDIUM) is a good balance. - Root stays on socket authentication for MariaDB and for MySQL on Ubuntu. That means
rootlogs in withsudoand no password, and cannot log into phpMyAdmin. This is intentional: you’ll create a dedicated application user in the next step and use that for your app and for phpMyAdmin.
When finished you should see a message like:
All done! If you've completed all of the above steps, your installation should now be secure.
C. Create the Application Database and User
Log in as root:
sudo mariadb # MariaDB (all distros)
sudo mysql # MySQL on Ubuntu
mysql -u root -p # MySQL on Rocky Linux
Create a database and a user for your app (replace the names and password):
CREATE DATABASE my_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'my_app_user'@'localhost' IDENTIFIED BY 'your-secure-password';
GRANT ALL PRIVILEGES ON my_app.* TO 'my_app_user'@'localhost';
EXIT;
Test the new login:
mysql -u my_app_user -p my_app
These are the values you’ll put in DB_DATABASE, DB_USER, and DB_PASSWORD in your .env file (Section 10B).
D. Optional: Give Root a Password
Only do this if you specifically need password login for root. The application user above is the recommended approach.
MariaDB (keeps socket login and adds a password):
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket OR mysql_native_password USING PASSWORD('your-secure-password');
MySQL on Ubuntu:
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'your-secure-password';
⚠️ Older guides use
IDENTIFIED WITH mysql_native_password. That plugin is disabled by default in MySQL 8.4 and removed in MySQL 9, so usecaching_sha2_passwordwith MySQL.
Verify:
SELECT user, host, plugin FROM mysql.user;
5. Install PHP Table of Contents
Chappy.php requires PHP 8.3 or newer. On Ubuntu and Debian, the PHP in the default repositories already meets that, so no third-party repository is needed. Rocky Linux uses the Remi repository.
| OS | PHP version installed |
|---|---|
| Ubuntu 24.04 | 8.3 |
| Ubuntu 26.04 | 8.5 |
| Debian 13 / LMDE 7 | 8.4 |
| Rocky Linux 9 / 10 (Remi) | 8.4 |
A. Ubuntu & Debian
sudo apt install -y php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-sqlite3 php-bcmath php-intl php-gd sqlite3
These version-independent package names install whatever PHP version your release ships, so the same command works on every Ubuntu and Debian version in this guide.
⚠️ Don’t install the bare
phppackage on a LEMP server. It pulls in Apache (libapache2-mod-php) by default, which will fight Nginx for port 80. Installingphp-fpmas shown avoids this.
The PHP-FPM service name includes the version. Find yours and make sure it’s running:
php -v # e.g. PHP 8.4.x → service is php8.4-fpm
sudo systemctl enable --now php<version>-fpm
B. Rocky Linux (RHEL-based)
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm
sudo dnf module reset php -y
sudo dnf module enable php:remi-8.4 -y # Enable PHP 8.4 from Remi
sudo dnf install -y php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysqlnd php-pdo php-bcmath php-gd php-opcache php-intl php-soap
sudo systemctl enable --now php-fpm
$(rpm -E %rhel) expands to 9 or 10, so the same command works on both Rocky versions.
On Rocky, PHP-FPM runs as the
apacheuser by default, even without Apache installed, and already grants Nginx access to its socket. This guide keeps that default, so the PHP-writablestoragedirectory uses theapachegroup (Section 10E).
C. Verify installation:
php -v
6. Configure Nginx and PHP-FPM Table of Contents
A. Create Temporary Location
We will temporarily create the location for our app so we can test PHP. It gets replaced by the real project in Section 10:
sudo mkdir -p /var/www/my-app
B. Set proper permissions:
Ubuntu and Debian
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 (replace my-app.local and my_ip_address with information relevant for your case):
server {
listen 80;
server_name my-app.local my_ip_address;
root /var/www/my-app;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.html index.htm index.php;
charset utf-8;
# Must be at least as large as PHP's post_max_size (Section 6E)
client_max_body_size 10M;
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$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
/run/php/php-fpm.sock is a version-independent link to the installed PHP-FPM socket, so this file doesn’t need editing when PHP is upgraded. If Nginx returns 502 Bad Gateway, see Troubleshooting B.
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 (replace my-app.local and my_ip_address with information relevant for your case):
server {
listen 80;
server_name my-app.local my_ip_address;
root /var/www/my-app;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.html index.htm index.php;
charset utf-8;
# Must be at least as large as PHP's post_max_size (Section 6E)
client_max_body_size 10M;
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$ {
try_files $uri =404;
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/PHP to connect to the database. Because DB_HOST=127.0.0.1 connects over TCP, SELinux must allow it:
sudo setsebool -P httpd_can_network_connect_db 1
Only if your app makes outbound connections (external APIs, SMTP, etc.):
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_can_sendmail 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 temporary directory after testing (the real project is moved here in Section 10):
sudo rm -rf /var/www/my-app/
E. Configure Upload Size (for Profile Image Support):
Ubuntu and Debian
The path includes your PHP version (for example 8.3, 8.4, or 8.5). Check it with php -v, then edit:
sudo vi /etc/php/<version>/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 values should be set depending on what type of files are 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.
Nginx has its own limit. client_max_body_size in your server block (Section 6C) must be at least as large as post_max_size, or Nginx will reject the upload with 413 Request Entity Too Large before PHP ever sees it. Its default is only 1M.
Then restart PHP-FPM:
Ubuntu and Debian
sudo systemctl restart php<version>-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 --no-install-recommends phpmyadmin
--no-install-recommends stops apt from pulling in Apache alongside phpMyAdmin.
When prompted:
- Do not select a web server (Nginx is not listed). Press Tab then Enter to continue with nothing selected.
- Choose Yes to configure dbconfig-common and create the phpMyAdmin database.
- Set a phpMyAdmin password or leave it blank to auto-generate.
- If you get
ERROR 1819 (HY000), see the Troubleshooting section.
Rocky Linux (RHEL-based)
sudo dnf install -y phpMyAdmin
# If the package isn't found:
sudo dnf --enablerepo=remi 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-available/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:/run/php/php-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 502 Bad Gateway error, see Troubleshooting B.
- 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 with the application user you created in Section 4C (for example my_app_user). root cannot log in through phpMyAdmin while it uses socket authentication. That’s expected.
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 LTS Node.js version.
A. Add Node.js Repository
Ubuntu and Debian
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash -
Rocky Linux (RHEL-based)
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
B. Install Node.js & NPM
Ubuntu and Debian
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 home directory and install dependencies:
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
B. Project Configuration
Open your preferred IDE (We use VSCode) and edit the .env file:
- Set
APP_DOMAINto/. - For security purposes ensure
DB_LOG_PARAMSis set tononeinstead offull. - Set
LOGGINGenvironmental variable as appropriate for your use. In production we recommendinfoto prevent excessive logging. - Update the database section with the database and user from Section 4C:
# Set to mysql or mariadb DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=my_app DB_USER=my_app_user DB_PASSWORD=your-secure-password
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 the server block or /etc/hosts:
sudo nginx -t
sudo systemctl restart nginx
Rocky Linux (RHEL-based)
1. Fix SELinux Contexts
Files moved from your home directory keep their old SELinux label, so Nginx and PHP-FPM can’t read them. Reset the labels:
sudo restorecon -Rv /var/www/my-app
2. Make Storage Writable
Mark storage as writable by PHP. This is persistent and survives reboots and relabels:
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.
Now restart Nginx:
sudo systemctl restart nginx
E. Final Steps
Set permissions and ownership. The storage directory must be writable by the PHP-FPM user for logs and uploads.
Ubuntu & Debian (PHP-FPM runs as www-data)
sudo chown -R your-username:www-data /var/www/my-app
Rocky Linux (RHEL-based) (PHP-FPM runs as apache)
sudo chown -R your-username:apache /var/www/my-app
All distros
sudo find /var/www/my-app -type d -exec chmod 755 {} \;
sudo find /var/www/my-app -type f -exec chmod 644 {} \;
sudo chmod -R 775 /var/www/my-app/storage
Create JavaScript bundle if necessary and run migrations:
cd /var/www/my-app
npm run build
php console migrate
Your project should now be accessible at:
http://<your_ip_address>
11. Troubleshooting Table of Contents
A. Database Issues
- ERROR 1819 (HY000) during phpMyAdmin installation: Your password does not meet MySQL’s VALIDATE PASSWORD policy. Use a stronger password, or remove the component:
UNINSTALL COMPONENT 'file://component_validate_password'; Access denied for user 'root'@'localhost'in phpMyAdmin: Expected when root uses socket authentication. Log in with your application user (Section 4C), or see Section 4D.Plugin 'mysql_native_password' is not loaded: You’re on MySQL 8.4+. Usecaching_sha2_passwordinstead (Section 4D).- App can’t connect to the database on Rocky Linux: Run
sudo setsebool -P httpd_can_network_connect_db 1(Section 6C).
B. 502 Bad Gateway
Nginx can’t reach PHP-FPM. Check that PHP-FPM is running and that the socket in your config exists:
Ubuntu & Debian
systemctl status php<version>-fpm
ls -l /run/php/
If php-fpm.sock isn’t listed, use the versioned socket shown (for example php8.4-fpm.sock) in your fastcgi_pass lines.
Rocky Linux
systemctl status php-fpm
ls -l /run/php-fpm/
Then check the Nginx error log:
sudo tail -n 50 /var/log/nginx/error.log
C. 413 Request Entity Too Large
Uploads are larger than Nginx allows. Increase client_max_body_size in your server block (Section 6C) and reload Nginx.
D. Port 80 Already in Use / Nginx Won’t Start
Apache may have been installed as a dependency (usually by installing the bare php package, or phpMyAdmin without --no-install-recommends). Remove it:
sudo apt purge -y apache2 libapache2-mod-php* # Ubuntu & Debian
sudo systemctl restart nginx
E. Finding SELinux Denials (Rocky Linux)
If you get 403 errors, blank pages, or write failures that permissions don’t explain, check for SELinux denials:
sudo ausearch -m avc -ts recent
F. Rocky Linux 10 Won’t Boot or Install
Rocky Linux 10 requires an x86-64-v3 CPU. On older hardware, use Rocky Linux 9. On a VM, change the CPU type to host (or another v3-capable model).
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 (LEMP)
C. How To Install and Secure phpMyAdmin with Nginx on an Ubuntu Server