Complete LNMP Stack Setup Guide: Linux + Nginx + MySQL + PHP from scratch
LNMP (Linux + Nginx + MySQL + PHP) is the classic server architecture for PHP websites — WordPress, Laravel, and ThinkPHP all run on it. The key difference from LAMP: Nginx handles static files and reverse proxying with an event-driven model that uses far less memory and handles concurrency better than Apache, while PHP is executed by a separate php-fpm process pool over FastCGI.
1. System Initialization
OS Selection
Go with Ubuntu 22.04 LTS or 24.04 LTS. LTS gets you 5+ years of security updates, and Ubuntu 24.04 ships PHP 8.3 by default, saving upgrade headaches. Debian 12 is also solid, but most docs and examples target Ubuntu — follow Ubuntu if you're new. On memory: 2 GB is enough to start for a personal site, while e-commerce or high-concurrency work should start at 4 GB to avoid a painful migration later.
Initial Security
Three things on a fresh box: update the system, create a normal user, and disable root SSH login.
# Update system
apt update && apt upgrade -y
# Create regular user
adduser deploy
usermod -aG sudo deploy
# Configure SSH key auth
ssh-keygen -t ed25519 -C "[email protected]"
# Add public key to ~/.ssh/authorized_keys
# Disable root login
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart sshd
# Configure firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
Reference: Ubuntu server security hardening https://ubuntu.com/server/docs/security-hardening; OpenSSH manual https://www.openssh.com/manual.html
Mind the order: confirm key login works before flipping PermitRootLogin no, or you'll be locked out and reduced to using your provider's rescue console.
Two more small things: set up swap — on a 1-2 GB machine without swap, memory spikes end in OOM kills; fallocate -l 2G /swapfile && mkswap /swapfile && swapon /swapfile gets you one (add it to /etc/fstab to mount on boot). And set the timezone (timedatectl set-timezone Asia/Shanghai), or log timestamps and cron jobs will never line up when you're debugging.
2. Install Nginx
apt install nginx -y
nginx -v
systemctl status nginx
Apply a few baseline tweaks in /etc/nginx/nginx.conf:
cat > /etc/nginx/nginx.conf << 'EOF'
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/x-component;
gzip_min_length 256;
gzip_comp_level 5;
gzip_vary on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
worker_processes auto spawns one worker per CPU core, and sendfile on enables zero-copy, which measurably boosts static-file throughput. Then create a site config that hands PHP requests to php-fpm:
cat > /etc/nginx/sites-available/example.com << 'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
# Enable site
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Reference: Nginx docs https://nginx.org/en/docs/; beginners guide https://nginx.org/en/docs/beginners_guide.html
3. Install MySQL
apt install mysql-server -y
mysql_secure_installation
Then create the database and user — and use utf8mb4 everywhere, not utf8, or emoji and rare characters will bite you later. Also don't use a weak password — generate a random string with openssl rand -base64 24 to stay safe from dictionary attacks.
mysql -u root -p
CREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
MySQL's defaults are conservative; tune at least these in production:
# /etc/mysql/mysql.conf.d/optimize.cnf
[mysqld]
# InnoDB settings
innodb_buffer_pool_size = 1G # ~60-70% of available memory
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_file_per_table = 1
# Connections
max_connections = 150
max_allowed_packet = 64M
# Cache
query_cache_type = 0 # Removed in MySQL 8.0
tmp_table_size = 64M
max_heap_table_size = 64M
# Charset
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size matters most — the official guidance is 60-70% of available RAM; too small and hot data lives on disk.
Reference: MySQL 8.0 docs https://dev.mysql.com/doc/refman/8.0/en/; InnoDB buffer pool https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
4. Install PHP
# Ubuntu 24.04 default PHP 8.3
apt install php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl \
php8.3-gd php8.3-mbstring php8.3-xml php8.3-bcmath php8.3-zip \
php8.3-redis php8.3-opcache -y
php -v
php8.3-mysql (database), php8.3-mbstring (multibyte strings) and php8.3-opcache (bytecode cache) are must-haves — WordPress and Laravel error out without them. Tune the php-fpm pool to your box:
# /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
# /etc/php/8.3/cli/conf.d/99-optimize.ini
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
date.timezone = Asia/Shanghai
pm.max_children isn't "the more the better" — multiply it by average memory per PHP process and keep it under ~70% of total RAM, or you'll hit OOM during peaks.
Reference: PHP manual https://www.php.net/manual/; php-fpm configuration https://www.php.net/manual/en/install.fpm.configuration.php
5. One-click Alternatives
Prefer not to type everything by hand? Use a one-click package. LNMP.org is a long-standing one-click installer:
wget https://soft.lnmp.com/lnmp/lnmp2.0.tar.gz
tar zxf lnmp2.0.tar.gz
cd lnmp2.0
./install.sh lnmp
Oneinstack offers more component options:
wget https://oneinstack.com/download/oneinstack-full.tar.gz
tar xzf oneinstack-full.tar.gz
cd oneinstack
./install.sh --nginx_option 1 --php_option 9 --mysql_option 2
Reference: LNMP.org https://lnmp.org; OneinStack https://oneinstack.com
One-click installers are convenient, follow tidy directory conventions, and include common security settings; the downside is that versions follow the author's cadence, and opaque build flags are hard to debug when something breaks. For a controlled production environment, install manually at least once — it builds real understanding of the whole chain.
6. Security Hardening
Fail2Ban blocks the vast majority of brute-force attempts:
# Install Fail2Ban
apt install fail2ban -y
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
maxretry = 5
bantime = 3600
[nginx-http-auth]
enabled = true
maxretry = 5
bantime = 3600
EOF
systemctl restart fail2ban
# Auto security updates
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Fail2Ban's core value is automatic bans like "five failures, one hour off," which combined with key auth blocks 99% of scripted scanning. Also confirm PasswordAuthentication no in /etc/ssh/sshd_config — with key-only login, brute force doesn't even get a chance to try.
7. Performance Testing
# Load test with ab
apt install apache2-utils -y
ab -n 1000 -c 50 http://example.com/
# PHP smoke test
cat > /var/www/example.com/public/info.php << 'EOF'
<?php
phpinfo();
EOF
After setup, run ab -n 1000 -c 50 to check QPS and response time, and watch memory with htop to confirm pm.max_children isn't eating all the RAM.
8. Choosing an Approach
The tradeoff boils down to one sentence: the more convenient the approach, the less you control when something breaks. Manual install costs the most time up front, but directories, permissions, and parameters are all in your hands, which makes debugging and tuning calmer later.
| Approach | Difficulty | Control | Who It's For |
|---|---|---|---|
| Manual install | High | High | Production, ops-minded teams |
| LNMP.org | Low | Medium | Personal sites wanting fast launch |
| Oneinstack | Low | Medium | Developers choosing component versions |
| Control panel (BT) | Lowest | Low | Non-CLI users |
16IDC Takeaway
LNMP is the foundation of PHP website hosting. 1 GB of RAM runs a basic stack, but a smooth WordPress experience needs 2 GB, and Laravel should start at 2 GB too. After installation, replace every example.com with your real domain and put HTTPS (Let's Encrypt free certs) in place before launch. Back up the database with a daily cron mysqldump to object storage — don't keep backups on the same box. For the best performance on the right hardware, check our server recommendations.