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.