All articles
LinuxServer ManagementNginxPHPDevOps

Linux + Nginx + PHP-FPM: My Production Server Setup

PKParth Kanasagra
10 min read

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.

Related Articles

PK

Parth Kanasagra

Certified Senior Laravel & PHP Developer · 9+ years experience · Available for hire