Production VPS Setup: Ubuntu, Nginx, PHP-FPM, MariaDB

Step-by-step guide to a production-ready Ubuntu VPS for PHP sites: users and SSH hardening, firewall, Nginx, PHP-FPM, MariaDB, free SSL with Certbot, and backups.

Illustration for: Production VPS Setup: Ubuntu, Nginx, PHP-FPM, MariaDB

Most of my freelance work ends on a VPS: a Laravel app, a WordPress site or an OpenCart store moving off shared hosting. This is the baseline I set up on a fresh Ubuntu 24.04 server before deploying anything.

1. First login: create a user and lock down SSH

adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh

Then in /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Restart with systemctl restart ssh, and test the new login in a second terminal before closing the first one.

2. Firewall and automatic security updates

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

apt install unattended-upgrades fail2ban -y
dpkg-reconfigure --priority=low unattended-upgrades

3. Nginx, PHP-FPM and MariaDB

apt update && apt upgrade -y
apt install nginx mariadb-server -y
apt install php8.3-fpm php8.3-mysql php8.3-xml php8.3-mbstring \
            php8.3-curl php8.3-zip php8.3-gd php8.3-intl -y
mysql_secure_installation

Create a separate database user for each site, and never connect an app as root:

CREATE DATABASE shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shop'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON shop.* TO 'shop'@'localhost';

4. A clean Nginx server block

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~* \.(jpg|jpeg|png|webp|svg|css|js|woff2)$ {
        expires 30d;
        access_log off;
    }

    location ~ /\.(?!well-known) { deny all; }
}

5. Free SSL with Certbot

apt install certbot python3-certbot-nginx -y
certbot --nginx -d example.com -d www.example.com

Certbot installs a renewal timer; check it with systemctl list-timers | grep certbot.

6. PHP-FPM and OPcache tuning

For a 2 GB VPS running one or two sites, start with conservative pm values and enable OPcache:

; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5

; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.validate_timestamps=0   ; reload php-fpm on deploy

7. Backups you have actually tested

A nightly database dump plus files, copied off the server:

#!/bin/bash
STAMP=$(date +%F)
mysqldump --single-transaction shop | gzip > /backups/shop-$STAMP.sql.gz
tar -czf /backups/files-$STAMP.tar.gz /var/www/example
rclone copy /backups remote:server-backups
find /backups -mtime +14 -delete
A backup you have never restored is only a hope. Do a test restore to a staging server at least once.

Prefer a control panel?

For clients who want a UI, I often use CloudPanel, Virtualmin or CyberPanel on top of the same hardening. The panel handles vhosts and SSL, but SSH keys, the firewall and backups are still your job.

Utsav Karki

Utsav Karki

MSc Computer Science student at Memorial University and Digital Systems & Web Intern at Verisult, based in St. John's, NL. I build with WordPress, Laravel, Next.js and Linux servers. Work with me →