Introduction
You just bought a VPS (Virtual Private Server). You have an IP address, a root password, and a blank Ubuntu server staring back at you. What do you do next?
This is the question most beginners struggle with. Tutorials online tend to jump straight into deploying applications without covering the foundational work that every production server needs before a single line of application code is deployed.
This guide covers everything you need to do after buying a VPS, in the correct order. By the end you will have a secure, hardened, production-ready server capable of hosting PHP/Laravel applications, Node.js APIs, Python apps, multiple domains, SSL certificates, and databases.
Everything in this guide is based on a real Ubuntu 24.04 LTS server setup. The same principles apply to any Ubuntu or Debian-based VPS from providers like Contabo, DigitalOcean, Linode, or Vultr.
What Is a VPS and Why Does the Setup Matter?
A VPS is a virtual machine running on shared physical hardware in a data centre. Unlike shared hosting where everything is managed for you, a VPS gives you full root access to a Linux server. You control everything.
That freedom comes with responsibility. A freshly provisioned VPS is:
- Accessible to the entire internet on port 22 (SSH)
- Running as root by default
- Accepting password-based logins
- Already being scanned by bots within minutes of going live
If you skip the setup and jump straight to deploying your application, you are building on an insecure foundation. A poorly configured VPS can be compromised within hours of going online.
The setup process is not optional. It is the difference between a server that gets hacked and a server that does not.
Phase 1: Inspect Your Server Before Touching Anything
The first rule of working on any server is to understand what you have before you change anything. When you first log in, run a series of inspection commands to build a picture of your server.
Connect via SSH
ssh root@YOUR_VPS_IP
Your VPS provider will give you the root password in a welcome email. Once connected you will see a prompt like:
root@vps-hostname:~#
You are now logged in as root on your server.
Check the Operating System
cat /etc/os-release && uname -r
This tells you the exact OS version and kernel. Everything else depends on this. Ubuntu 24.04 LTS is the recommended choice for production servers as it is supported until April 2029.
Check CPU, RAM, and Disk
lscpu | grep -E "^CPU\(s\)|^Model name|^Thread|^Core"
free -h
df -h
Note how many CPU cores you have, how much RAM is available, and how much disk space you are working with. These numbers affect how you configure PHP workers, database buffers, and swap space later.
Check What Is Already Running
ss -tlnp
systemctl list-units --type=service --state=running
On a clean VPS from a reputable provider you should see almost nothing running except SSH. If you see MySQL, Redis, or a web server already running, note it before proceeding.
Check the Firewall
ufw status verbose
On most fresh Ubuntu VPS instances the firewall is installed but inactive. This means the only thing protecting your server right now is that nothing unexpected is listening. We will fix this shortly.
Check SSH Configuration
sshd -T | grep -E "^permitrootlogin|^passwordauthentication|^pubkeyauthentication|^port"
On a fresh VPS you will typically see:
permitrootlogin yes
passwordauthentication yes
pubkeyauthentication yes
port 22
This means root can log in with a password over the public internet. This is the most dangerous default configuration on any server and the first thing we will fix.
Phase 2: Secure SSH Access
SSH (Secure Shell) is the gateway to your server. Securing it is the single most important step in this entire guide.
Why Root Login Is Dangerous
When you log in as root, every command you run has unlimited power. A typo, a mistake, or a compromised session can destroy your entire server instantly. Worse, root is a known username that attackers target specifically. Brute force bots attempt thousands of root password combinations every hour against any server with port 22 open.
The solution is to create a separate administrative user that uses SSH keys instead of passwords.
Create a Non-Root Admin User
adduser yourname
usermod -aG sudo yourname
Choose a username that is not obvious. Avoid admin, ubuntu, or user as these are common brute force targets.
Set Up SSH Key Authentication
SSH keys are cryptographic key pairs. Your private key stays on your local machine. Your public key is installed on the server. When you connect, SSH verifies the keys match without ever transmitting a password.
On your local machine, check if you already have a key:
ls ~/.ssh/
If you see id_rsa and id_rsa.pub or id_ed25519 and id_ed25519.pub you already have a key pair. If not, generate one:
ssh-keygen -t ed25519 -C "your-email@example.com"
Ed25519 is the modern recommended key type. It is faster and more secure than the older RSA format.
View your public key:
cat ~/.ssh/id_ed25519.pub
The output is a long string starting with ssh-ed25519. This is safe to share openly.
Install the public key on the server by running this as root on the VPS:
mkdir -p /home/yourname/.ssh
echo "YOUR_PUBLIC_KEY_HERE" >> /home/yourname/.ssh/authorized_keys
chown -R yourname:yourname /home/yourname/.ssh
chmod 700 /home/yourname/.ssh
chmod 600 /home/yourname/.ssh/authorized_keys
The permissions are critical. SSH will refuse to use the key if the permissions are too open. 700 on the directory means only the owner can access it. 600 on the file means only the owner can read it.
Test the New User Before Changing Anything
Open a second SSH session and test logging in as your new user:
ssh yourname@YOUR_VPS_IP
You should be logged in without entering a password. Then verify sudo works:
sudo whoami
It should return root.
Do not close your original root session until this test passes. This is your safety net.
Harden the SSH Configuration
Only after confirming your new user works, back up and edit the SSH configuration:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo nano /etc/ssh/sshd_config
Change these settings:
| Setting | Change to |
|---|---|
PermitRootLogin yes |
PermitRootLogin no |
PasswordAuthentication yes |
PasswordAuthentication no |
X11Forwarding yes |
X11Forwarding no |
#MaxAuthTries 6 |
MaxAuthTries 3 |
#LoginGraceTime 2m |
LoginGraceTime 30 |
On Ubuntu 24.04, also check the override files in /etc/ssh/sshd_config.d/. Files in this directory can override settings in the main config. Make sure PasswordAuthentication no is consistent across all files in that directory.
Test the configuration before applying it:
sudo sshd -t
Silence means the configuration is valid. Then reload:
sudo systemctl reload ssh
Test root login from a new terminal. You should see Permission denied (publickey). Root is now blocked.
What If You Get Locked Out?
Every major VPS provider offers a web-based console in their control panel. This gives you direct access to the server regardless of SSH configuration. If you ever lock yourself out, log into your provider's control panel and use the console to fix the SSH config.
Phase 3: Configure the Firewall
Ubuntu ships with UFW (Uncomplicated Firewall). It is inactive by default. Configure and enable it before installing anything else.
Why You Need a Firewall
As you install software, some services will listen on public ports by default. MySQL, Redis, and PostgreSQL have all been compromised on servers where they were accidentally left exposed. A firewall provides a second layer of protection even when a service is misconfigured.
Set Default Policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
Deny everything coming in, allow everything going out. Then explicitly allow only what needs to be public.
Allow Only What Is Necessary
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
Three rules. SSH for administration, HTTP for web traffic and SSL certificate validation, HTTPS for encrypted web traffic.
Never open these ports publicly:
| Port | Service | Reason to keep it closed |
|---|---|---|
| 3306 | MySQL | No application needs direct public database access |
| 5432 | PostgreSQL | Same reason |
| 6379 | Redis | Redis with no password and public access leads to immediate compromise |
| 3000 | Node.js | App servers go behind Nginx, never directly public |
| 8000 | Python | Same reason |
Enable the Firewall
sudo ufw enable
Verify the rules:
sudo ufw status verbose
You should see only ports 22, 80, and 443 allowed. Everything else is blocked.
Phase 4: Harden the Server
With SSH secured and the firewall active, harden the server itself.
Add a Swap File
A swap file is disk space used as overflow when RAM fills up. Without it, the Linux OOM (Out of Memory) killer will terminate processes randomly when memory runs out. On a server this usually means your database or web server gets killed unexpectedly.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
The last command makes swap permanent across reboots. Also reduce swappiness so the kernel prefers RAM over swap:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl vm.swappiness=10
Set a Meaningful Hostname
sudo hostnamectl set-hostname your-server-name
Then update the hosts file:
sudo nano /etc/hosts
Find the line with your old hostname and replace it with the new one. A clear hostname like production-01 or app-server makes logs easier to read, especially when managing multiple servers.
Enable Automatic Security Updates
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Select Yes when prompted. This automatically installs security patches without requiring manual intervention. It applies security updates only, not major version upgrades, so it will not break your applications.
Disable Unnecessary Services
Ubuntu Server ships with services that serve no purpose on a VPS:
sudo systemctl stop ModemManager multipathd udisks2
sudo systemctl disable ModemManager multipathd udisks2
ModemManager handles mobile modems. multipathd manages multipath storage for enterprise SAN systems. udisks2 manages removable drives. None of these belong on a web server. Removing them reduces memory usage and attack surface.
Apply Kernel Hardening
sudo nano /etc/sysctl.d/99-hardening.conf
Add these settings:
# IP Spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Block SYN flood attacks
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
# Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Hide kernel pointers
kernel.kptr_restrict = 2
Apply immediately:
sudo sysctl -p /etc/sysctl.d/99-hardening.conf
These settings protect against common network attacks including IP spoofing, SYN floods, and ICMP redirect attacks.
Phase 5: Install Nginx as Your Reverse Proxy
Nginx sits in front of all your applications. The internet talks to Nginx. Nginx decides where to send each request.
sudo apt install nginx -y
sudo systemctl enable nginx
The Nginx Architecture
Internet
|
v
Nginx (port 80/443)
|
+-- example.com --> PHP-FPM (Laravel)
|
+-- api.example.com --> Node.js on port 3000
|
+-- app.example.com --> Python on port 8000
Your applications never talk to the internet directly. Only Nginx does. This gives you SSL in one place, security headers in one place, and the ability to run many applications on a single server.
Harden the Default Nginx Configuration
Open the main config:
sudo nano /etc/nginx/nginx.conf
Inside the http {} block, make these changes:
Hide the Nginx version number from response headers:
server_tokens off;
Enable gzip compression:
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml;
Set sensible timeouts and upload limits:
client_max_body_size 64M;
client_body_timeout 30;
keepalive_timeout 65;
send_timeout 30;
Disable the Default Site
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Create Your Application Directories
sudo mkdir -p /var/www/example.com/public
sudo chown -R youruser:youruser /var/www/example.com
Create a Server Block for Each Site
For a Laravel application:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/current/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
For a Node.js API:
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log /var/log/nginx/api.example.com.access.log;
error_log /var/log/nginx/api.example.com.error.log;
}
Enable each site and test:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Always run nginx -t before reloading. If the test fails, Nginx keeps the old working configuration running.
Phase 6: Install Your Application Stack
PHP 8.3 and Composer
sudo apt install -y php8.3 php8.3-fpm php8.3-cli \
php8.3-mysql php8.3-pgsql php8.3-mbstring php8.3-xml \
php8.3-curl php8.3-zip php8.3-bcmath php8.3-opcache \
php8.3-intl php8.3-gd
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
sudo chmod +x /usr/local/bin/composer
Node.js LTS
Do not install Node.js from Ubuntu's default repositories. The version there is outdated. Use NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Python and Virtual Environment Tools
Ubuntu 24.04 ships with Python 3.12. Add the tooling:
sudo apt install -y python3-pip python3-venv python3-dev
Always use virtual environments for Python applications. Never install application packages system-wide as this creates conflicts between projects.
Phase 7: Install and Secure Your Databases
MySQL
sudo apt install -y mysql-server
sudo mysql_secure_installation
During secure installation answer the prompts like this:
- Validate password component: No
- Remove anonymous users: Yes
- Disallow root login remotely: Yes
- Remove test database: Yes
- Reload privilege tables: Yes
PostgreSQL
sudo apt install -y postgresql postgresql-contrib
Redis
sudo apt install -y redis-server
sudo sed -i 's/^bind 127.0.0.1 ::1/bind 127.0.0.1/' /etc/redis/redis.conf
sudo systemctl restart redis-server
sudo systemctl enable redis-server
The sed command restricts Redis to localhost only. Redis with no password and public internet access is one of the most common and damaging server compromises in existence. Always keep it bound to 127.0.0.1.
Accessing Databases Remotely
Your databases are not publicly accessible and should stay that way. To connect from your local machine using a tool like TablePlus, DBeaver, or HeidiSQL, use an SSH tunnel:
# MySQL tunnel
ssh -L 3306:127.0.0.1:3306 youruser@YOUR_VPS_IP -N
# PostgreSQL tunnel
ssh -L 5432:127.0.0.1:5432 youruser@YOUR_VPS_IP -N
Then connect your database client to 127.0.0.1 on the forwarded port. The connection travels through your encrypted SSH session. No database port is ever exposed to the internet.
Phase 8: Set Up Backups
A server with no backup strategy is a server waiting to fail. Set this up before deploying anything.
Create a backup script:
sudo mkdir -p /opt/backups/files
sudo nano /opt/backups/backup.sh
Add this content:
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backups/files"
# MySQL
mysqldump --all-databases --single-transaction | gzip > "$BACKUP_DIR/mysql_$DATE.sql.gz"
# PostgreSQL
sudo -u postgres pg_dumpall | gzip > "$BACKUP_DIR/postgres_$DATE.sql.gz"
# Nginx config
tar -czf "$BACKUP_DIR/nginx_$DATE.tar.gz" /etc/nginx/
# Remove backups older than 7 days
find "$BACKUP_DIR" -type f -mtime +7 -delete
Make it executable and schedule it:
sudo chmod +x /opt/backups/backup.sh
sudo crontab -e
Add this line to run every night at 2 AM:
0 2 * * * /opt/backups/backup.sh >> /var/log/backup.log 2>&1
Backups that exist only on the same server are not real backups. Set up offsite backup to Google Drive using rclone, to AWS S3, or to a separate server. If your VPS is compromised or the data centre has an incident, local-only backups go with it.
Phase 9: Monitor Your Server
Native Linux Tools
These are available immediately with no installation:
# Interactive process monitor
htop
# Disk usage
df -h
# Memory usage
free -h
# Real time Nginx access log
sudo tail -f /var/log/nginx/example.com.access.log
# Real time error log
sudo tail -f /var/log/nginx/example.com.error.log
# System logs
sudo journalctl -f
# Active listening ports
sudo ss -tlnp
Netdata Dashboard
Netdata gives you a real-time visual dashboard for CPU, RAM, disk, Nginx, MySQL, Redis, and PHP-FPM all in one place:
curl https://get.netdata.cloud/kickstart.sh > /tmp/netdata-kickstart.sh
sudo sh /tmp/netdata-kickstart.sh --stable-channel --disable-telemetry
Netdata runs on port 19999. Do not open this port publicly. Access it via SSH tunnel:
ssh -L 19999:localhost:19999 youruser@YOUR_VPS_IP -N
Then visit http://localhost:19999 in your browser for the full dashboard.
External Uptime Monitoring
Sign up for a free account at UptimeRobot. Add your domain and it checks every 5 minutes, sending you an email or SMS the moment your site goes down. This catches server crashes, Nginx failures, and network issues before your customers do.
The Pre-Deployment Checklist
Before pointing any domain at this server or deploying any application, run through this checklist:
Security
- [ ] Root SSH login is disabled
- [ ] Password authentication is disabled
- [ ] SSH key authentication is confirmed working
- [ ] Admin user has sudo access
- [ ] Firewall is active with only ports 22, 80, and 443 open
- [ ] No database ports are publicly accessible
- [ ] Redis is bound to localhost only
- [ ] Kernel hardening parameters are applied
- [ ] Unnecessary services are disabled
Stack
- [ ] Nginx is running and enabled on boot
- [ ] PHP-FPM is running and enabled on boot
- [ ] Composer is installed
- [ ] Node.js and npm are installed
- [ ] Python and venv tools are installed
- [ ] MySQL is installed and secured
- [ ] PostgreSQL is installed
- [ ] Redis is installed and localhost-only
Infrastructure
- [ ] 2GB swap file is active and permanent
- [ ] Hostname is set correctly
- [ ] Automatic security updates are enabled
- [ ] Backup script is created and scheduled
- [ ] Backup script has been tested manually at least once
- [ ] Offsite backup destination is configured
- [ ] Uptime monitoring is active
Nginx
- [ ] Default site is disabled
- [ ] Server blocks exist for all your domains
- [ ]
nginx -tpasses with no errors - [ ]
server_tokens offis set - [ ] Security headers are in all server blocks
- [ ] Log files are configured per site
What Comes Next
Once your checklist is complete your server is production ready. Here is what to do next:
Point your DNS first. Update your domain's A record to point to your server IP at your registrar. DNS propagation takes anywhere from a few minutes to 48 hours depending on your provider.
Install SSL certificates. Once DNS is pointing to your server, run Certbot to get free Let's Encrypt certificates for each domain. This takes under two minutes per domain.
Deploy your applications. Each application type has its own deployment process. Treat each one as a separate focused task rather than trying to deploy everything at once.
Create application databases. For each app, create a dedicated database user with only the permissions that app needs. Never use the root database user for application connections.
Common Mistakes to Avoid
Skipping the firewall. Every day you run without a firewall is a day your databases could be accidentally exposed. Enable UFW before installing any software.
Deploying as root. Applications should run as dedicated low-privilege users. Running as root means a compromised application has unlimited access to your entire server.
Using root for database connections. Always create application-specific database users with minimal permissions. The MySQL or PostgreSQL root user should never appear in an application config file.
Storing secrets in Git. Your .env file contains database passwords, API keys, and application secrets. It must never be committed to version control. Add .env to .gitignore before your first commit.
Skipping backups until later. Later never comes. Set up backups before your first deployment. The worst time to realise you have no backups is after something breaks.
Opening database ports publicly. Use SSH tunneling for database access from your local machine. Ports 3306 and 5432 should never appear in your UFW allowed rules.
Summary
Setting up a VPS correctly is not complicated but it requires doing things in the right order. Here is the sequence that matters:
- Inspect the server before changing anything
- Create a non-root admin user with SSH key authentication
- Test the new user, then disable root login and password auth
- Configure and enable the firewall before installing any software
- Harden the server with swap, kernel parameters, and automatic updates
- Install Nginx as a reverse proxy
- Install your application stack: PHP, Node.js, Python
- Install and secure your databases: MySQL, PostgreSQL, Redis
- Set up automated backups with offsite storage
- Set up monitoring and uptime alerts
Only after completing all of these steps should you point a domain to the server and begin deploying applications.
The time you invest in this setup pays back every single day your server runs without incident. A server configured this way can host multiple production applications for years without requiring significant maintenance or firefighting.
For technical assistance whatsapp me here
This guide is part of a series on production server management. Other articles in the series cover Nginx as a reverse proxy in depth, setting up free SSL certificates with Let's Encrypt, and deploying Laravel applications with zero-downtime releases.
Top comments (0)