DEV Community

Sospeter Mong'are
Sospeter Mong'are

Posted on

How to Set Up a VPS Server From Scratch (Complete Beginner's Guide)

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
Enter fullscreen mode Exit fullscreen mode

Your VPS provider will give you the root password in a welcome email. Once connected you will see a prompt like:

root@vps-hostname:~#
Enter fullscreen mode Exit fullscreen mode

You are now logged in as root on your server.

Check the Operating System

cat /etc/os-release && uname -r
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

On a fresh VPS you will typically see:

permitrootlogin yes
passwordauthentication yes
pubkeyauthentication yes
port 22
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should be logged in without entering a password. Then verify sudo works:

sudo whoami
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Silence means the configuration is valid. Then reload:

sudo systemctl reload ssh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Verify the rules:

sudo ufw status verbose
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Set a Meaningful Hostname

sudo hostnamectl set-hostname your-server-name
Enter fullscreen mode Exit fullscreen mode

Then update the hosts file:

sudo nano /etc/hosts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply immediately:

sudo sysctl -p /etc/sysctl.d/99-hardening.conf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inside the http {} block, make these changes:

Hide the Nginx version number from response headers:

server_tokens off;
Enter fullscreen mode Exit fullscreen mode

Enable gzip compression:

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml;
Enter fullscreen mode Exit fullscreen mode

Set sensible timeouts and upload limits:

client_max_body_size 64M;
client_body_timeout 30;
keepalive_timeout 65;
send_timeout 30;
Enter fullscreen mode Exit fullscreen mode

Disable the Default Site

sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Create Your Application Directories

sudo mkdir -p /var/www/example.com/public
sudo chown -R youruser:youruser /var/www/example.com
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Make it executable and schedule it:

sudo chmod +x /opt/backups/backup.sh
sudo crontab -e
Enter fullscreen mode Exit fullscreen mode

Add this line to run every night at 2 AM:

0 2 * * * /opt/backups/backup.sh >> /var/log/backup.log 2>&1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 -t passes with no errors
  • [ ] server_tokens off is 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:

  1. Inspect the server before changing anything
  2. Create a non-root admin user with SSH key authentication
  3. Test the new user, then disable root login and password auth
  4. Configure and enable the firewall before installing any software
  5. Harden the server with swap, kernel parameters, and automatic updates
  6. Install Nginx as a reverse proxy
  7. Install your application stack: PHP, Node.js, Python
  8. Install and secure your databases: MySQL, PostgreSQL, Redis
  9. Set up automated backups with offsite storage
  10. 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)