Linux + Nginx + PHP-FPM: My Production Server Setup
I set up a new VPS three to four times a year. For the first couple of years I did it from memory and would forget something — usually the firewall, once the fail2ban config that would have caught a brute force attack three days later. Now I have a checklist.
Initial Hardening: Non-Root User + SSH Key Only
# On the fresh server as root
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cat your_public_key >> /home/deploy/.ssh/authorized_keys
chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Port 2222 # Non-standard port reduces bot noise
Firewall with UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # SSH (your custom port)
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
fail2ban Against Brute Force
apt install fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2222
maxretry = 5
bantime = 1h
Nginx for PHP Apps
server {
listen 80;
server_name example.com;
root /var/www/myapp/public;
index index.php;
client_max_body_size 50M;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~* \.(css|js|png|jpg|woff2|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
PHP-FPM Tuning
# /etc/php/8.4/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500 # Restart workers after N requests — prevents memory leaks
Formula for max_children: (available_RAM - OS_overhead) / avg_PHP_memory_per_process. For 2GB server with 50MB per PHP process: roughly 32 workers.
OPcache
; /etc/php/8.4/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 0 ; 0 = never recheck files (safe in prod)
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 128M
SSL with Certbot
apt install certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com
Auto-renewal is configured automatically. Add ssl_protocols TLSv1.2 TLSv1.3; to Nginx config to disable older TLS versions.