Deploying a web application on a VPS can feel overwhelming, especially if you’re new to server setup, DNS, and security. In this guide, I’ll walk you through the exact steps I use every time I deploy a real project, from server creation to HTTPS setup, so you can deploy yours quickly and securely.
Why Production Deployment Matters
Most tutorials either focus only on code or ignore real-world security essentials. This article covers everything you actually need for a production-ready environment, leveraging the same high-performance principles I used for my Multi-Tenant CRM project.
Tools & Requirements
Before we start, ensure you have the following in your modern developer toolkit:
- A VPS provider (e.g., DigitalOcean, Vultr, or Hetzner)
- A domain name
- SSH client (Terminal / PowerShell / PuTTY)
- Basic knowledge of your app stack (PHP, Node.js, Laravel etc.)
Step 1: Create & Configure Your VPS
- Choose a Linux distro (Ubuntu 24.04 recommended).
- Create a new server with at least 1GB RAM.
- Add a SSH Key (security best practice).
- Select a region closest to your users for low-latency.
- Launch the instance and note your public IP.
Step 2: Connect via SSH
Open your terminal and establish a secure connection:
ssh root@your_server_ip
Step 3: Secure the Server (Hardening)
Update packages and create a non-root user for security:
sudo apt update && sudo apt upgrade -y
adduser devuser
usermod -aG sudo devuser
Enable the firewall to block unauthorized access:
ufw allow OpenSSH
ufw enable
Step 4: DNS Setup
Map your domain to your new server IP at your domain registrar. Add an 'A' record pointing '@' to your server IP. For custom infrastructure assistance, explore my professional web services.
Step 5: Install Nginx Web Server
sudo apt install nginx
sudo ufw allow 'Nginx Full'
Step 6: Deploy Your App Logic
Whether you're deploying a PHP/Laravel app or a Node.js ecosystem, ensure proper directory permissions:
sudo chown -R www-data:www-data /var/www/yourapp
Step 7: Universal SSL with Let’s Encrypt
Security is non-negotiable. Automate your HTTPS setup using Certbot:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx
Testing & Authority Scaling
Always verify every route and monitor your error logs to ensure a frictionless user experience. This attention to detail is what defines technical excellence.
Deploying a web application on a VPS can feel overwhelming, especially if you're new to server setup, DNS, and security. In this guide, I'll walk you through the exact steps I use every time I deploy a real project — from server creation to automated HTTPS setup — so you can go from local development to a live, production-ready environment quickly and securely.
This is not a beginner overview. Every command in this guide is production-tested. I used this exact workflow to deploy my Multi-Tenant CRM handling 1.5M+ leads, my real-time Chatrox WebSocket chat application, and multiple client projects running in Pakistan, UAE, and the UK.
What You Will Learn
- How to create and harden an Ubuntu 24.04 VPS from scratch
- Full Nginx server block configuration for PHP/Laravel and Node.js apps
- PHP 8.3 + PHP-FPM setup for production performance
- MySQL 8.0 secure installation and database creation
- Automated SSL with Let's Encrypt Certbot
- Git-based deployment workflow — push to deploy
- Environment file (.env) security best practices
- Common errors and how to fix them
Tools & Requirements
Before we start, ensure you have the following ready. These are the same tools in my 2026 developer toolkit:
- VPS Provider: DigitalOcean, Vultr, Hetzner, or Linode — any works. Minimum 1GB RAM, 1 vCPU, 25GB SSD. I recommend Hetzner for cost — €4/month for 2GB RAM.
- OS: Ubuntu 24.04 LTS (Long Term Support — stable and supported until 2029)
- Domain name: Any registrar — Namecheap, GoDaddy, or Cloudflare
- SSH client: Terminal on Mac/Linux, PowerShell or PuTTY on Windows
- Your app stack: This guide covers PHP/Laravel and Node.js. React/Next.js is also covered in Step 6.
Step 1: Create & Configure Your VPS
Log into your VPS provider dashboard and create a new server (called a "Droplet" on DigitalOcean or "Cloud Server" on Hetzner).
Recommended settings:
- OS: Ubuntu 24.04 LTS x64
- RAM: 1GB minimum for small apps, 2GB for Laravel with Redis
- Region: Choose closest to your primary users. For Pakistan clients, I use Frankfurt (EU) — better latency than US East for South Asian traffic.
- Authentication: Select SSH Key (not password — passwords are insecure and brute-forced constantly)
How to generate an SSH key if you don't have one:
# Run this on your LOCAL machine (not the server)
ssh-keygen -t ed25519 -C "[email protected]"
# This creates two files:
# ~/.ssh/id_ed25519 (private key — never share this)
# ~/.ssh/id_ed25519.pub (public key — paste this into your VPS provider)
# Copy your public key to clipboard (Mac/Linux):
cat ~/.ssh/id_ed25519.pub
Paste the output into your VPS provider's SSH key field. Once the server is created, note down your server's public IP address — you'll need it for every step.
Step 2: Connect via SSH & Initial Login
Open your terminal and connect to the server as root:
ssh root@YOUR_SERVER_IP
If you get a fingerprint warning, type yes and press Enter — this is normal on first connection. You should now see the Ubuntu welcome message. If you get "Permission denied (publickey)", your SSH key wasn't added correctly — go back and re-add it in the provider dashboard.
Step 3: Harden the Server (Do Not Skip This)
Every public-facing server gets attacked within minutes of creation. These steps protect you against 99% of automated attacks.
3a. Update All Packages
apt update && apt upgrade -y
What this does: Downloads and installs all security patches. The -y flag auto-confirms all prompts. This can take 2-5 minutes on first run.
3b. Create a Non-Root User
Running everything as root is dangerous — one mistake and you can destroy the server. Create a dedicated user:
# Replace 'devuser' with your preferred username
adduser devuser
# Add to sudo group so they can run admin commands
usermod -aG sudo devuser
# Switch to the new user
su - devuser
Set a strong password when prompted. From now on, use this user instead of root.
3c. Copy SSH Key to New User
# Run this as root (switch back temporarily)
su - root
rsync --archive --chown=devuser:devuser ~/.ssh /home/devuser
3d. Configure the Firewall
# Allow SSH (critical — do this BEFORE enabling firewall)
ufw allow OpenSSH
# Allow HTTP and HTTPS
ufw allow 80
ufw allow 443
# Enable the firewall
ufw enable
# Verify rules
ufw status
Expected output:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
If you don't see OpenSSH allowed before running ufw enable, you will lock yourself out of the server permanently.
3e. Disable Root SSH Login
sudo nano /etc/ssh/sshd_config
Find these lines and change them:
# Change from:
PermitRootLogin yes
# Change to:
PermitRootLogin no
PasswordAuthentication no
Save with Ctrl+X, then Y, then Enter. Restart SSH:
sudo systemctl restart ssh
Step 4: DNS Setup
Your domain registrar's DNS panel needs an A record pointing to your server IP. Log into your domain registrar (Namecheap, GoDaddy, Cloudflare etc.) and add:
Type: A
Host: @ (represents your root domain, e.g. yourdomain.com)
Value: YOUR_SERVER_IP
TTL: 300 (5 minutes — use low TTL while setting up, increase later)
# Also add for www:
Type: A
Host: www
Value: YOUR_SERVER_IP
TTL: 300
Verify DNS propagation (can take 5-30 minutes):
# On your LOCAL machine:
ping yourdomain.com
# Or use dig:
dig yourdomain.com A +short
You should see your server IP returned. If you see a different IP or "NXDOMAIN", the DNS has not propagated yet — wait 10 minutes and try again. Do NOT proceed to SSL setup until DNS resolves correctly.
Step 5: Install Nginx Web Server
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx # Auto-start on reboot
Test that Nginx is running:
sudo systemctl status nginx
You should see Active: active (running). Open your server IP in a browser — you should see the Nginx welcome page.
Full Nginx Server Block for Laravel/PHP
This is the production-ready Nginx configuration I use for every Laravel project. It is not the same as the basic config most tutorials show:
sudo nano /etc/nginx/sites-available/yourdomain.com
Paste this complete server block:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# Document root — for Laravel, point to /public
root /var/www/yourdomain/public;
index index.php index.html;
# Laravel pretty URLs
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM configuration
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# Block access to sensitive files
location ~ /\.(?!well-known).* {
deny all;
}
# Block access to .env files
location ~ /\.env {
deny all;
return 404;
}
# Gzip compression for performance
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
# Logs
access_log /var/log/nginx/yourdomain.access.log;
error_log /var/log/nginx/yourdomain.error.log;
}
Enable the site and test the config:
# Create symlink to enable site
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
# Remove default site
sudo rm /etc/nginx/sites-enabled/default
# Test config syntax — must say "test is successful"
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
If nginx -t shows any errors, read the error message carefully — it will tell you exactly which line has the problem.
Nginx Config for Node.js / Next.js Apps
If you are deploying a Node.js or Next.js app, use this reverse proxy config instead:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000; # Your Node.js port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
Step 6: Install PHP 8.3 + PHP-FPM (Laravel Apps)
Skip this step if you are deploying a Node.js app.
# Add PHP repository
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
# Install PHP 8.3 and all extensions Laravel needs
sudo apt install php8.3 php8.3-fpm php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-bcmath php8.3-curl php8.3-zip php8.3-gd \
php8.3-redis php8.3-intl -y
# Verify installation
php --version
Expected output: PHP 8.3.x (cli)
Configure PHP-FPM for Production
sudo nano /etc/php/8.3/fpm/php.ini
Find and update these values for production:
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120
memory_limit = 256M
expose_php = Off # Hides PHP version from headers — security
Restart PHP-FPM:
sudo systemctl restart php8.3-fpm
sudo systemctl enable php8.3-fpm
Step 7: Install MySQL 8.0 & Create Database
sudo apt install mysql-server -y
sudo mysql_secure_installation
The secure installation wizard will ask several questions. Recommended answers:
- VALIDATE PASSWORD component: Y
- Password strength: 2 (strong)
- Remove anonymous users: Y
- Disallow root login remotely: Y
- Remove test database: Y
- Reload privilege tables: Y
Create your application database and user:
sudo mysql -u root -p
# Inside MySQL prompt:
CREATE DATABASE yourapp_db;
CREATE USER 'yourapp_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON yourapp_db.* TO 'yourapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Never use the root MySQL user for your application. Always create a dedicated user with access only to the specific database.
Step 8: Deploy Your Application Code
Create the directory structure and deploy your code:
# Create web directory
sudo mkdir -p /var/www/yourdomain
sudo chown -R devuser:www-data /var/www/yourdomain
sudo chmod -R 755 /var/www/yourdomain
Git-Based Deployment (Recommended)
This is the deployment method I use for all client projects — push to Git, server pulls automatically:
# Clone your repository
cd /var/www/yourdomain
git clone https://github.com/yourusername/your-repo.git .
# For Laravel: install dependencies
sudo apt install composer -y
composer install --no-dev --optimize-autoloader
Environment File (.env) Setup
Never commit your .env file to Git. Create it directly on the server:
cp .env.example .env
nano .env
Update these critical values:
APP_ENV=production
APP_DEBUG=false # CRITICAL — never true in production
APP_URL=https://yourdomain.com
DB_HOST=127.0.0.1
DB_DATABASE=yourapp_db
DB_USERNAME=yourapp_user
DB_PASSWORD=StrongPassword123!
Security rule: APP_DEBUG=false in production is non-negotiable. Debug mode exposes your database credentials, file paths, and full stack traces to anyone who triggers an error.
Laravel Final Setup Commands
# Generate application key
php artisan key:generate
# Run database migrations
php artisan migrate --force
# Optimize for production
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Set correct permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
Step 9: SSL Certificate with Let's Encrypt (Free HTTPS)
HTTPS is mandatory — Google ranks HTTP sites lower and Chrome shows "Not Secure" warnings. Let's Encrypt provides free, auto-renewing certificates.
sudo apt install certbot python3-certbot-nginx -y
# Get certificate (replace with your actual domain)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot will ask for your email address and whether to redirect HTTP to HTTPS. Choose 2 (Redirect) for automatic HTTP-to-HTTPS redirection.
Expected output:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/yourdomain.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/yourdomain.com/privkey.pem
This certificate expires on 2026-08-24.
Deploying certificate to VirtualHost /etc/nginx/sites-enabled/yourdomain.com
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/yourdomain.com
Certbot automatically auto-renews certificates before they expire. Test auto-renewal:
sudo certbot renew --dry-run
If you see "Congratulations, all renewals succeeded", your SSL is set up correctly and will never expire.
Step 10: Set Up Automated Git Deployment
This step is optional but saves enormous time. Create a deploy script so you can update your live site with one command:
sudo nano /var/www/yourdomain/deploy.sh
Paste this script:
#!/bin/bash
# Deploy script for yourdomain.com
# Usage: bash deploy.sh
echo "=== Starting deployment ==="
cd /var/www/yourdomain
# Pull latest code
git pull origin main
# Install/update PHP dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Fix permissions
sudo chown -R www-data:www-data storage bootstrap/cache
# Restart PHP-FPM
sudo systemctl reload php8.3-fpm
echo "=== Deployment complete ==="
Make it executable:
chmod +x /var/www/yourdomain/deploy.sh
Now to deploy updates, just run:
bash /var/www/yourdomain/deploy.sh
Common Errors & How to Fix Them
These are the errors I encounter most frequently — and their exact fixes:
Error: "502 Bad Gateway"
This means Nginx cannot connect to PHP-FPM. Check that PHP-FPM is running and the socket path matches your Nginx config:
# Check PHP-FPM status
sudo systemctl status php8.3-fpm
# Verify socket file exists
ls -la /var/run/php/php8.3-fpm.sock
# Restart PHP-FPM
sudo systemctl restart php8.3-fpm
Error: "403 Forbidden"
File permission issue. Nginx cannot read your files:
sudo chown -R www-data:www-data /var/www/yourdomain
sudo chmod -R 755 /var/www/yourdomain
sudo chmod -R 775 /var/www/yourdomain/storage
Error: "500 Internal Server Error" (Laravel)
Check the Laravel error log:
tail -n 50 /var/www/yourdomain/storage/logs/laravel.log
Most common cause: APP_KEY not set. Run php artisan key:generate.
Error: SSL Certificate Not Working
Certbot failed because DNS hasn't propagated yet. Wait for DNS to resolve, then run:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com --force-renewal
Error: "MySQL Access Denied"
Wrong credentials in .env. Verify your database user:
sudo mysql -u yourapp_user -p yourapp_db
If this fails, reset the password:
sudo mysql -u root -p
ALTER USER 'yourapp_user'@'localhost' IDENTIFIED BY 'NewPassword123!';
FLUSH PRIVILEGES;
Performance Monitoring & Log Checking
After deployment, monitor your server health regularly:
# Real-time server resource usage
htop
# Check Nginx error logs (most useful for debugging)
sudo tail -f /var/log/nginx/yourdomain.error.log
# Check Nginx access logs
sudo tail -f /var/log/nginx/yourdomain.access.log
# Check system disk usage
df -h
# Check RAM usage
free -m
Set up a free uptime monitor at UptimeRobot.com — it pings your site every 5 minutes and emails you if it goes down. Takes 2 minutes to set up and has saved me from extended downtime multiple times.
Security Checklist Before Going Live
Run through this checklist before sending any traffic to your new server:
- ✅ SSH password login disabled
- ✅ Root SSH login disabled
- ✅ UFW firewall active — only ports 22, 80, 443 open
- ✅ APP_DEBUG=false in .env
- ✅ .env file not accessible via browser (Nginx blocks it)
- ✅ HTTPS active and HTTP redirects to HTTPS
- ✅ Database user has only permissions it needs (not root)
- ✅ Storage and bootstrap/cache directories writable by www-data
- ✅ All packages updated (apt update && apt upgrade)
Conclusion
You now have a production-ready server with Ubuntu 24.04, Nginx, PHP 8.3 + PHP-FPM, MySQL 8.0, free SSL, and an automated deployment script. This exact setup powers real applications handling thousands of concurrent users.
The difference between a tutorial deployment and a production deployment is not complexity — it is attention to the security hardening steps that most guides skip. Disabling root SSH, setting APP_DEBUG=false, creating database-specific users, and blocking .env access are the details that separate professional deployments from vulnerable ones.
If you are building a custom web application and need professional deployment and architecture support, I offer custom software development services for teams in Pakistan, UAE, UK, and globally. My projects include systems managing over 1.5 million database records in production — the same principles in this guide, applied at scale.
Have questions about a specific step? Reach out through the contact page or connect on LinkedIn.