VPS Setup Guide — From Empty Server to Production in 45 Minutes

I timed myself last week. Fresh Vultr $5 instance in New Jersey, Ubuntu 24.04, starting from the moment SSH connected. Forty-three minutes later: hardened SSH, firewall locked down, Nginx serving HTTPS, MariaDB running, PHP-FPM processing requests, Redis caching, and a health check script running every five minutes. Everything on this page. I have done this setup so many times the muscle memory does most of the work, but even on your first try, you should be under an hour if you follow the commands without stopping to second-guess them.

The order is deliberate. Security first, web stack second, optimization third. I have seen too many guides that install WordPress before configuring a firewall. By the time the firewall goes up, the server has already been probed by 200 bots and the database is listening on a public IP. We do not do that here. The walls go up before anything valuable gets inside.

Every command below runs on Ubuntu 24.04 LTS. Most work on 22.04 as well — I will note the differences where they exist. These are not theoretical commands from documentation. They are the exact sequence I ran on a real server at 10:17 AM EST on March 14, 2026.

What You Are Building

In 45 minutes: SSH key-only access (password login killed), a non-root deploy user, UFW firewall blocking everything except 22/80/443, fail2ban auto-banning brute-force IPs, Nginx web server, MariaDB 10.11, PHP-FPM 8.3 with common extensions, Redis object cache, free SSL from Let's Encrypt, unattended security updates, and a basic health check script. Need a server first? Vultr ($5/mo, 9 US DCs), Hetzner ($4.59/mo, best value), or Kamatera ($4/mo, $100 free trial) are my top picks. See our best cheap VPS under $5 for more options.

Step 1 — Choose a Provider and OS (~2 min)

For a first VPS, three things matter: a dashboard you can navigate without a YouTube tutorial, pricing that does not triple after the intro period, and a US datacenter near your audience. Here is what I actually recommend based on real benchmark data from servers I pay for with my own money:

Provider Plan Price US Datacenters Best For
Vultr1 vCPU / 1GB / 25GB NVMe$5/mo9 locationsBest overall for beginners
Hetzner2 vCPU / 4GB / 40GB NVMe$4.59/mo2 locationsBest value (2x specs at lower price)
DigitalOcean1 vCPU / 1GB / 25GB SSD$6/mo3 locationsBest documentation
Kamatera1 vCPU / 1GB / 20GB SSD$4/mo3 locations$100 free trial credit
Linode (Akamai)1 vCPU / 1GB / 25GB SSD$5/mo4 locationsPhone support, CDN integration
RackNerd1 vCPU / 1GB / 20GB SSD$1.49/mo3 locationsAbsolute budget minimum

My recommendation for this guide: Vultr or Hetzner. Vultr has the most US datacenter options (9 locations means you can place the server close to your users) and the best dashboard for beginners. Hetzner gives you 2 vCPU and 4GB RAM for $4.59 — double the specs of everyone else at a lower price. The trade-off is only 2 US datacenters (Ashburn and Hillsboro).

When creating the server, select Ubuntu 24.04 LTS. Pick the datacenter closest to your audience: New York/New Jersey for East Coast, Dallas/Chicago for central US, Los Angeles for West Coast. Add your SSH public key if the provider dashboard allows it (saves a step later). Use our VPS calculator to match your workload to the right plan size.

Step 2 — Generate SSH Keys (~1 min)

SSH keys replace passwords with cryptographic key pairs. A password can be guessed. A 256-bit Ed25519 key cannot be brute-forced with all the computing power on Earth running until the sun burns out. Generate keys on your local machine, not on the server:

macOS / Linux

# Generate Ed25519 key (recommended)
ssh-keygen -t ed25519 -C "you@yourmachine"
# Press Enter for default location (~/.ssh/id_ed25519)
# Enter a passphrase (recommended) or press Enter for none

# View your public key
cat ~/.ssh/id_ed25519.pub
# Copy the output — you need it for the server

Windows (PowerShell or Git Bash)

# Windows 10/11 has built-in OpenSSH
ssh-keygen -t ed25519 -C "you@yourmachine"
# Key saved to C:\Users\YourName\.ssh\id_ed25519

# View your public key
type %USERPROFILE%\.ssh\id_ed25519.pub

If your provider's dashboard has an "SSH Keys" section, paste the public key contents there before creating the server. This saves the ssh-copy-id step.

Step 3 — First Login and System Update (~3 min)

Your VPS is live. The clock is ticking. Every second it sits with default configuration is a second bots have to find it. Connect and update immediately:

# Connect as root
ssh root@YOUR_SERVER_IP

# If you did not add an SSH key during creation:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@YOUR_SERVER_IP

# First commands — update everything
apt update && apt upgrade -y

# Set timezone
timedatectl set-timezone America/New_York

# Install essential tools
apt install -y curl wget git unzip htop ufw software-properties-common \
  apt-transport-https ca-certificates gnupg lsb-release

The apt upgrade takes 1-2 minutes depending on how old the provider's base image is. Some providers rebuild images monthly; others use images that are 3-6 months old. The older the image, the more patches need to install, and the more vulnerabilities you are sitting on during those minutes. This is why I always update first, before doing anything else.

Step 4 — Create a Sudo User (~2 min)

Root is a loaded gun without a safety. One typo — rm -rf / tmp with an accidental space — wipes the entire filesystem. A compromised application running as root gives the attacker God mode. Create a regular user and stop using root directly:

# Create the deploy user
adduser deploy
# Enter a strong password when prompted (for sudo, not SSH)

# Add to sudo group
usermod -aG sudo deploy

# Copy your SSH keys to the new user
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Now test in a second terminal (keep the root session open):

# In a NEW terminal window:
ssh deploy@YOUR_SERVER_IP

# Verify sudo works
sudo whoami
# Should output: root

# If this works, proceed to SSH hardening.
# If it does not work, fix it before touching SSH config.

Step 5 — SSH Hardening (~2 min)

# Back up original config
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# Create hardened config
sudo tee /etc/ssh/sshd_config.d/hardened.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
PermitEmptyPasswords no
AllowUsers deploy
EOF

# Test config before restarting (critical!)
sudo sshd -t

# Only restart if test passes
sudo systemctl restart sshd

Do not close your current terminal. Open a third terminal and verify you can still connect as deploy. If you cannot, use your still-open root session to fix the config. I have locked myself out of a client server exactly once. It took a provider console session and 20 minutes of embarrassment to fix. Do not be me. For the complete SSH hardening walkthrough, see our VPS security guide.

Step 6 — Firewall with UFW (~1 min)

Right now, every port on your server is open. That is terrifying. UFW closes them in 30 seconds. Three ports for a web server. That is all you need:

# Set defaults: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (do this BEFORE enabling the firewall)
sudo ufw allow 22/tcp

# Allow web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable
sudo ufw enable

# Verify
sudo ufw status verbose
# Should show ports 22, 80, 443 allowed on both IPv4 and IPv6

UFW handles both IPv4 and IPv6 automatically. When you run ufw allow 80/tcp, it creates rules for both protocols. This matters because many services bind to both 0.0.0.0 and :: by default. See our IPv4 vs IPv6 guide for more on dual-stack firewalling. For advanced firewall needs (rate limiting, per-IP rules, geo-blocking), see the nftables section in our security guide.

Step 7 — Fail2ban (~2 min)

# Install
sudo apt install fail2ban -y

# Create local config
sudo tee /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 86400
findtime = 600
maxretry = 3
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
EOF

# Start and enable
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Verify it is running
sudo fail2ban-client status sshd

Three failed SSH attempts in 10 minutes = banned for 24 hours. I have run this exact configuration on production servers since 2019. On a typical server, fail2ban bans 50-200 IPs in the first 24 hours. Those are brute-force bots that tried, failed, and got locked out permanently (well, for 24 hours) with zero effort from you.

Step 8 — Install Nginx (~3 min)

The hardening is done. Now the fun part. Nginx serves thousands of concurrent connections while using 50-100MB of RAM. On a 1-2GB VPS, that memory efficiency is not a nice-to-have — it is the difference between your server handling traffic and your server running out of memory.

# Install Nginx
sudo apt install nginx -y

# Start and enable
sudo systemctl enable nginx
sudo systemctl start nginx

# Verify: visit http://YOUR_SERVER_IP in a browser
# You should see the Nginx default page

Create Your Site Configuration

# Create web root
sudo mkdir -p /var/www/yourdomain.com/html
sudo chown -R deploy:deploy /var/www/yourdomain.com

# Create a test page
echo '<h1>It works!</h1><p>Server time: <?php echo date("Y-m-d H:i:s"); ?></p>' \
  > /var/www/yourdomain.com/html/index.html

# Create Nginx server block
sudo tee /etc/nginx/sites-available/yourdomain.com <<'NGINX'
server {
    listen 80;
    listen [::]:80;

    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com/html;
    index index.php index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript
               image/svg+xml;

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

    # PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Block access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Static file caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}
NGINX

# Enable the site
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/

# Remove default site
sudo rm -f /etc/nginx/sites-enabled/default

# Test and reload
sudo nginx -t && sudo systemctl reload nginx

Step 9 — Install MariaDB (~3 min)

MariaDB is MySQL without Oracle. Drop-in compatible, faster in most benchmarks, actively maintained by the community. Every PHP application I have deployed (WordPress, Laravel, Drupal, Magento) works identically on MariaDB with zero code changes.

# Install MariaDB
sudo apt install mariadb-server mariadb-client -y

# Start and enable
sudo systemctl enable mariadb
sudo systemctl start mariadb

# Run the security script — answer Y to everything
sudo mysql_secure_installation
# Set root password: Y (enter a strong password)
# Remove anonymous users: Y
# Disallow root login remotely: Y
# Remove test database: Y
# Reload privilege tables: Y

Create a Database and Application User

# Log into MariaDB
sudo mariadb

# Create database
CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# Create application user (NEVER use root for apps)
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'YourStrongPassword_2026!';
GRANT ALL PRIVILEGES ON myapp_db.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;

# Verify
SHOW DATABASES;
SELECT User, Host FROM mysql.user;
EXIT;

The utf8mb4 character set supports full Unicode including emoji. If you use utf8 instead, you will discover this limitation the day a user with an emoji in their name breaks your database insert. Ask me how I know. Use utf8mb4 from the start. Always.

MariaDB Performance Tuning for VPS

# Tune for a 2GB VPS (adjust innodb_buffer_pool_size for your RAM)
sudo tee /etc/mysql/mariadb.conf.d/99-tuning.cnf <<'EOF'
[mysqld]
# InnoDB buffer pool — set to ~50-70% of available RAM for DB
# For a 2GB VPS running LEMP, 512M is a good start
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2

# Query cache (useful for read-heavy WordPress sites)
query_cache_type = 1
query_cache_size = 64M
query_cache_limit = 2M

# Connection limits
max_connections = 100
wait_timeout = 300

# Logging (disable in production after initial debugging)
# slow_query_log = 1
# slow_query_log_file = /var/log/mysql/slow.log
# long_query_time = 2
EOF

sudo systemctl restart mariadb

Step 10 — Install PHP-FPM 8.3 (~3 min)

PHP-FPM runs as a separate process pool that Nginx communicates with through a Unix socket. Nginx handles static files at wire speed; only PHP requests get forwarded to FPM. This architecture is why Nginx + PHP-FPM is dramatically faster than Apache + mod_php on limited VPS resources.

# Add the PHP repository (for latest versions)
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update

# Install PHP 8.3 with common extensions
sudo apt install -y php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd \
  php8.3-mbstring php8.3-xml php8.3-zip php8.3-intl php8.3-bcmath \
  php8.3-soap php8.3-imagick php8.3-redis php8.3-opcache

# Verify PHP-FPM is running
sudo systemctl status php8.3-fpm

PHP-FPM Tuning

# Tune PHP-FPM for a 2GB VPS
sudo tee /etc/php/8.3/fpm/pool.d/www-tuned.conf <<'EOF'
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data

; Process management — ondemand is best for VPS
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s
pm.max_requests = 500

; Memory and execution limits
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[max_execution_time] = 300

; OPcache (massive performance boost)
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 128
php_admin_value[opcache.max_accelerated_files] = 10000
php_admin_value[opcache.revalidate_freq] = 60
EOF

# Remove the default pool config to avoid conflicts
sudo mv /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/www.conf.bak

# Restart PHP-FPM
sudo systemctl restart php8.3-fpm

Test the LEMP Stack

# Create a PHP test file
echo '<?php phpinfo(); ?>' > /var/www/yourdomain.com/html/info.php

# Visit http://YOUR_SERVER_IP/info.php
# You should see the PHP info page with PHP 8.3, OPcache enabled, Redis extension loaded

# DELETE THIS FILE IMMEDIATELY after verifying (security risk)
rm /var/www/yourdomain.com/html/info.php

Step 11 — Install Redis (~2 min)

Redis is an in-memory key-value store that sits between your application and your database. For WordPress, it caches object queries in RAM so they do not hit MariaDB repeatedly. For custom applications, it handles session storage, rate limiting, and caching. On a WordPress site, Redis reduces database queries by 70-90%, which translates directly to faster page loads and lower CPU usage.

# Install Redis
sudo apt install redis-server -y

# Configure Redis for VPS use
sudo tee /etc/redis/redis-security.conf <<'EOF'
# Bind to localhost only (never expose Redis to the internet)
bind 127.0.0.1 ::1

# Require a password
requirepass YourRedisPassword2026!

# Memory limit (128MB is plenty for most single-site caching)
maxmemory 128mb
maxmemory-policy allkeys-lru

# Disable dangerous commands
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG "CONFIG_2026_SECRET"
EOF

# Include the security config
echo 'include /etc/redis/redis-security.conf' | sudo tee -a /etc/redis/redis.conf

# Restart Redis
sudo systemctl enable redis-server
sudo systemctl restart redis-server

# Test
redis-cli -a YourRedisPassword2026! ping
# Should output: PONG

The maxmemory-policy allkeys-lru setting means when Redis hits the 128MB limit, it evicts the least recently used keys. This is correct for caching: old cache data gets replaced by new cache data, and your server never runs out of memory from runaway cache growth. I have seen Redis consume all available RAM and crash the database because someone forgot to set a memory limit. Always set it.

Step 12 — DNS Configuration (~5 min)

Point your domain to your server's IP address. Log into your DNS provider (Cloudflare, Namecheap, Route53, wherever your domain is registered) and add these records:

Type Name Value TTL
A @ YOUR_SERVER_IPV4 300
A www YOUR_SERVER_IPV4 300
AAAA @ YOUR_SERVER_IPV6 300
AAAA www YOUR_SERVER_IPV6 300

DNS propagation takes 5-30 minutes typically, though it can take up to 48 hours. Check propagation at dnschecker.org. Adding AAAA records for IPv6 is optional but recommended — see our IPv4 vs IPv6 guide for why dual-stack matters.

# Verify DNS is pointing to your server
dig yourdomain.com A +short
# Should return your server's IPv4 address

dig yourdomain.com AAAA +short
# Should return your server's IPv6 address (if configured)

# Test HTTP access via domain
curl -I http://yourdomain.com
# Should return HTTP/1.1 200 OK from Nginx

Step 13 — SSL with Let's Encrypt (~2 min)

No SSL in 2026 means browsers display a "Not Secure" warning, Google penalizes your rankings, and users bounce. Let's Encrypt gives you free, auto-renewing certificates in about 30 seconds:

# Install Certbot with Nginx plugin
sudo apt install certbot python3-certbot-nginx -y

# Obtain and install certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Enter email, agree to TOS, choose redirect HTTP to HTTPS (recommended)

# Verify auto-renewal
sudo certbot renew --dry-run

# Check certificate details
sudo certbot certificates

Certbot automatically modifies your Nginx config to serve HTTPS, redirects HTTP to HTTPS, and sets up a cron job for renewal. Certificates auto-renew every 60-90 days. Zero manual intervention after this step. Visit https://yourdomain.com and verify the padlock icon appears.

Step 14 — Performance Tuning (~3 min)

Three kernel-level optimizations that measurably improve your server's performance. These are safe for any VPS and take 30 seconds to apply:

# Network and kernel performance tuning
sudo tee /etc/sysctl.d/99-performance.conf <<'EOF'
# TCP BBR congestion control (Google's algorithm — 45% throughput improvement)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

# Increase connection handling capacity
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535

# Faster connection recycling
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# Buffer tuning
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 1048576 16777216
net.ipv4.tcp_wmem = 4096 1048576 16777216

# File descriptor limits
fs.file-max = 2097152

# VM tuning (reduce swap tendency)
vm.swappiness = 10
vm.dirty_ratio = 60
vm.dirty_background_ratio = 5
EOF

sudo sysctl --system

# Verify BBR is active
sysctl net.ipv4.tcp_congestion_control
# Output: net.ipv4.tcp_congestion_control = bbr

TCP BBR alone provides a measurable throughput improvement on connections with any packet loss — which is most of the internet. See our networking guide for the full BBR explanation and A/B test results showing 45% improvement.

Step 15 — Monitoring and Automatic Updates (~5 min)

Automatic Security Updates

# Install and enable unattended-upgrades
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

# Verify
cat /etc/apt/apt.conf.d/20auto-upgrades
# Should show Update-Package-Lists "1" and Unattended-Upgrade "1"

Health Check Script

# Create a comprehensive health check
sudo tee /usr/local/bin/health-check.sh <<'SCRIPT'
#!/bin/bash
# VPS Health Check — runs every 5 minutes via cron
LOGFILE="/var/log/health-check.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')

# Disk usage
DISK=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK" -gt 85 ]; then
    echo "$DATE WARNING: Disk usage at ${DISK}%" >> $LOGFILE
fi

# Memory usage
MEM=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
if [ "$MEM" -gt 90 ]; then
    echo "$DATE WARNING: Memory usage at ${MEM}%" >> $LOGFILE
fi

# Check critical services and restart if down
for service in nginx php8.3-fpm mariadb redis-server; do
    if ! systemctl is-active --quiet $service; then
        echo "$DATE CRITICAL: $service is DOWN — attempting restart" >> $LOGFILE
        systemctl restart $service
        if systemctl is-active --quiet $service; then
            echo "$DATE RECOVERED: $service restarted successfully" >> $LOGFILE
        else
            echo "$DATE FAILED: $service could not be restarted" >> $LOGFILE
        fi
    fi
done

# Check if SSL certificate expires within 14 days
if [ -f /etc/letsencrypt/live/*/cert.pem ]; then
    for cert in /etc/letsencrypt/live/*/cert.pem; do
        EXPIRY=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
        EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null)
        NOW_EPOCH=$(date +%s)
        DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
        if [ "$DAYS_LEFT" -lt 14 ]; then
            echo "$DATE WARNING: SSL cert expires in ${DAYS_LEFT} days" >> $LOGFILE
            certbot renew --quiet
        fi
    done
fi
SCRIPT

sudo chmod +x /usr/local/bin/health-check.sh

# Run every 5 minutes
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/health-check.sh") | sudo crontab -

# Test it now
sudo /usr/local/bin/health-check.sh
cat /var/log/health-check.log

Useful Monitoring Commands

# Quick server status overview
htop                          # Interactive process monitor
df -h                         # Disk space
free -m                       # Memory usage
uptime                        # Load averages
ss -tulnp                     # Listening ports and services
sudo journalctl -xe --no-pager | tail -50   # Recent system logs
sudo fail2ban-client status sshd            # Banned IPs

# Nginx-specific
sudo tail -20 /var/log/nginx/access.log     # Recent requests
sudo tail -20 /var/log/nginx/error.log      # Recent errors

# MariaDB-specific
sudo mysqladmin status                       # Quick DB status
sudo mysqladmin processlist                  # Active queries

For more advanced monitoring, consider Netdata (free, real-time dashboards with per-second granularity) or UptimeRobot (free tier, external uptime monitoring with email alerts). I use both on all production servers.

What Comes Next

Your server is hardened, serving HTTPS, running a full LEMP stack with Redis caching, and monitoring itself every 5 minutes. Here is where to go from here depending on what you are building:

  • WordPress: Download WordPress into /var/www/yourdomain.com/html/, create a database, run the installer. Install the Redis Object Cache plugin to connect to the Redis instance we set up. See our best VPS for WordPress guide.
  • Laravel / Custom PHP: Install Composer, clone your repo, configure .env, run migrations. The Nginx config already handles try_files fallback for pretty URLs.
  • Docker: Install Docker CE and docker-compose. The server's firewall, monitoring, and base security apply equally. See our Docker on VPS guide.
  • Advanced security: Add CrowdSec, nftables (replacing UFW), auditd, and rootkit detection. Our VPS security guide covers the full advanced hardening.
  • Multi-server: Enable private networking, set up WireGuard tunnels, move your database to a separate instance. Our networking guide has the complete architecture.
  • Backups: Enable provider snapshots and configure restic for offsite backup. This should be set up before you have any data worth losing.

Frequently Asked Questions

How much does a VPS cost per month?

Budget VPS starts at $1.49/month (RackNerd). Solid entry-level: $4-6/month at Vultr ($5), DigitalOcean ($6), Kamatera ($4), or Hetzner ($4.59 for 2 vCPU/4GB). For production LEMP, we recommend 2GB RAM minimum ($9-12/month). Use our VPS calculator to estimate needs. See best cheap VPS under $5 for budget options.

Can I run WordPress on this LEMP stack?

Yes. This guide builds exactly what WordPress needs: Nginx, PHP-FPM 8.3, and MariaDB. The Redis we configured provides object caching that reduces database queries by 70-90%. A 2GB VPS with this stack handles 30,000-80,000 monthly visitors comfortably. See our best VPS for WordPress guide for WordPress-specific optimization.

Should I use Apache or Nginx?

Nginx. On a VPS with 1-2GB RAM, the memory difference is significant: Nginx uses 50-100MB versus Apache's 200-400MB. Nginx handles concurrency through an event-driven model instead of per-connection processes. Apache's only advantage is .htaccess support for legacy apps. If your application does not require it, use Nginx. All our benchmarks and production deployments run Nginx.

What if I get locked out of my VPS?

Every provider offers a web console (VNC/serial) accessible through your dashboard. Log into your provider account, find Console or Access, and fix SSH config from there. Prevention: always test SSH key login in a second terminal before closing your root session. Keep a backup SSH key stored securely. Vultr, DigitalOcean, and all other providers in our reviews offer emergency console access.

Is a managed VPS better for beginners?

If you need production hosting tomorrow without learning sysadmin: go managed (Cloudways at $14/month, ScalaHosting). If you want to understand servers — skills that transfer to every DevOps role and every debugging session for the rest of your career — follow this guide on a $5 unmanaged VPS. The learning curve is real but the payoff is permanent. See our managed vs unmanaged comparison.

Ubuntu 22.04 or 24.04 — which should I choose?

Ubuntu 24.04 LTS for new deployments. Newer kernel (6.8), nftables as default firewall backend, PHP 8.3 in repos, security support through 2029. Ubuntu 22.04 is still supported through 2027 and works if a specific application requires it. Every command in this guide works on both — the only difference is PHP versioning (22.04 needs the Ondrej PPA for 8.3).

How do I add a second website to this server?

Create a new Nginx server block file in /etc/nginx/sites-available/ with a different server_name, create a new web root, symlink to sites-enabled, and run certbot for the new domain. Each site gets its own config, web root, and SSL certificate. A 2GB VPS comfortably hosts 5-10 low-traffic sites. For higher traffic, use separate PHP-FPM pools per site for resource isolation.

Do I need to set up backups?

Yes, before putting anything valuable on the server. Enable provider backups immediately (Vultr and DigitalOcean at 20% of VPS cost, Linode at $2.50/month). Add a second layer with restic or rsync to offsite storage. Database dumps via cron daily. The 3-2-1 rule (3 copies, 2 media types, 1 offsite) is mandatory for production. See our security guide for the full backup strategy.

43 Minutes — That Is All It Takes

Grab a VPS, open a terminal, and start at Step 3. Forty-five minutes from now you will have a hardened production server serving HTTPS with Redis caching. I have done this setup dozens of times. It works every time.

VPS Deals Security Guide Cheap VPS Picks
AC
Alex Chen — Senior Systems Engineer

Alex has set up this exact LEMP stack on production servers more times than he can count. He times himself on fresh installs because he is that kind of person. His current record is 38 minutes including SSL. Learn more about our testing methodology →