You've just spun up a fresh VPS from your provider. The dashboard says it's running, and you've got a root password in your inbox. Now what?
A raw server is like an unlocked house with no furniture—you need to secure the doors, set up rooms, and move in your stuff before anyone else does. I'll walk you through the seven steps I follow every time I provision a new VPS, from locking down SSH to deploying a live application. These aren't theoretical best practices; they're the exact sequence that keeps support tickets from piling up later.
Step 1: Generate and Configure SSH Keys
Password authentication is the first thing attackers try. SSH keys are faster, more secure, and let you skip typing passwords every time you connect.
On your local machine (Linux, macOS, or WSL), open a terminal and run:
ssh-keygen -t ed25519 -C "[email protected]"
Press Enter to accept the default file location (~/.ssh/id_ed25519). Set a passphrase when prompted—it's optional but adds a second layer if someone steals your private key. You'll see two files: id_ed25519 (private, keep this safe) and id_ed25519.pub (public, this goes on the server).
If you're on an older system that doesn't support Ed25519, fall back to RSA with at least 4096 bits:
ssh-keygen -t rsa -b 4096 -C "[email protected]"
Now copy the public key to your VPS. Replace your_server_ip with the actual IP:
ssh-copy-id root@your_server_ip
Enter the root password when prompted. The utility appends your public key to ~/.ssh/authorized_keys on the server. Test the connection:
ssh root@your_server_ip
If you log in without typing a password, you're set. If not, double-check the key was added to ~/.ssh/authorized_keys and that the file permissions are 600.
Step 2: Create a Non-Root User with Sudo Access
Running everything as root is risky. One typo in a script can wipe system files. Create a regular user and grant it sudo privileges for administrative tasks.
Log in as root and add a new user (replace deploy with your preferred username):
adduser deploy
You'll be prompted for a password and optional details. Then add the user to the sudo group:
usermod -aG sudo deploy
On some distributions the group is called wheel instead of sudo—check with groups deploy after running the command. Now copy your SSH key to the new user's home directory:
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
Test the new user in a separate terminal window (don't close your root session yet):
ssh deploy@your_server_ip
sudo ls /root
Enter the user's password when sudo prompts. If you see the contents of /root, sudo is working. From now on, use this account instead of root.
Step 3: Harden SSH Configuration
Default SSH settings allow password login on port 22, which is convenient for attackers. Disable password authentication and optionally move SSH to a non-standard port.
Edit the SSH daemon config:
sudo nano /etc/ssh/sshd_config
Find or add these lines:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Port 2222
Changing the port to something other than 22 won't stop determined attackers, but it drops the noise from automated scanners. Pick a number above 1024 that's not already in use (check with sudo ss -tulpn | grep :2222). Just remember to update your firewall rules and SSH client config.
Save and close the file, then restart SSH:
sudo systemctl restart sshd
Do NOT close your current session yet. Open a new terminal and test the new settings:
ssh -p 2222 deploy@your_server_ip
If you're locked out, go back to your original root session and revert the changes. Once you confirm the new config works, you can safely close the root session.
Step 4: Configure the Firewall
An open VPS listens on every port by default. Use ufw (Uncomplicated Firewall) to block everything except the services you need.
Install ufw if it's not already present:
sudo apt update
sudo apt install ufw
Before enabling the firewall, allow SSH on your chosen port (if you kept it at 22, adjust accordingly):
sudo ufw allow 2222/tcp
If you skip this step and enable ufw, you'll lock yourself out. Add rules for HTTP and HTTPS if you're running a web server:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Now enable the firewall:
sudo ufw enable
Confirm the rules:
sudo ufw status
You should see your SSH port, 80, and 443 listed as ALLOW. For database servers, mail relays, or custom applications, add rules for those ports as needed. Keep the default policy to deny all incoming connections; explicitly open only what you use.
Step 5: Update the System and Install Essential Packages
Distribution images are often weeks or months old. Update everything before you install applications.
On Debian or Ubuntu:
sudo apt update
sudo apt upgrade -y
On CentOS, Rocky, or AlmaLinux:
sudo dnf upgrade -y
The -y flag skips confirmation prompts. If a kernel update is included, reboot afterward:
sudo reboot
Wait a minute, then reconnect. Now install baseline utilities you'll need for troubleshooting and building software:
sudo apt install -y curl wget git vim htop build-essential
Adjust the package names for your distribution if needed. build-essential on Debian/Ubuntu provides gcc, make, and other compilation tools; on RHEL-based systems, use sudo dnf groupinstall "Development Tools".
Step 6: Install a Web Stack (Nginx, Database, Runtime)
Your application needs somewhere to run. I'll use Nginx, PostgreSQL, and Node.js as an example, but swap in Apache, MySQL, PHP, Python, or whatever your stack requires.
Install Nginx:
sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Visit http://your_server_ip in a browser. You should see the default Nginx welcome page. If you don't, check the firewall (ufw port 80) and Nginx logs (sudo journalctl -u nginx).
Install PostgreSQL:
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable postgresql
sudo systemctl start postgresql
Switch to the postgres user and create a database and user for your application:
sudo -u postgres psql
Inside the psql prompt:
CREATE DATABASE myapp;
CREATE USER myappuser WITH PASSWORD 'secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE myapp TO myappuser;
\q
Replace myapp, myappuser, and the password with your actual values. For Node.js, install it via NodeSource or use a version manager like nvm. Here's the NodeSource approach for the LTS version:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
node --version
If you see a version number, Node is installed. For production workloads, also install a process manager like PM2:
sudo npm install -g pm2
Step 7: Deploy and Run Your Application
Clone your application repository, install dependencies, configure environment variables, and start the service. I'll assume a Node.js app, but the pattern is similar for other runtimes.
Clone your repo (or upload files via scp/rsync):
cd /home/deploy
git clone https://github.com/yourusername/your-app.git
cd your-app
Install dependencies:
npm install --production
Create a .env file for secrets and config:
nano .env
Add your database credentials, API keys, and any other environment variables:
DATABASE_URL=postgresql://myappuser:secure_password_here@localhost/myapp
NODE_ENV=production
PORT=3000
Save and close. Start the app with PM2:
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup
The pm2 startup command prints a line you need to run with sudo—copy and execute it. This ensures PM2 restarts your app after a reboot.
Configure Nginx as a reverse proxy. Create a new site config:
sudo nano /etc/nginx/sites-available/myapp
Paste this template (adjust the domain and port):
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable the site and reload Nginx:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Point your domain's A record to the VPS IP, wait for DNS to propagate (usually a few minutes), then visit your domain. You should see your app. If you get a 502 Bad Gateway, the app isn't listening on the port Nginx expects—check pm2 logs and the .env file.
What happens when SSH keys stop working?
In support tickets I've handled, the usual culprit is incorrect permissions on ~/.ssh or authorized_keys. SSH is picky: the .ssh directory must be 700, and authorized_keys must be 600. If someone edited the file as root and didn't fix ownership, the SSH daemon ignores it.
Another common issue is SELinux on RHEL-based systems. Check the audit log:
sudo ausearch -m avc -ts recent
If you see denials related to SSH, restore the correct context:
sudo restorecon -Rv ~/.ssh
Finally, verify the key fingerprint matches. On your local machine:
ssh-keygen -lf ~/.ssh/id_ed25519.pub
On the server:
ssh-keygen -lf ~/.ssh/authorized_keys
If the fingerprints differ, you copied the wrong key.
Next steps after your first deploy
Your VPS is live, locked down, and serving traffic. From here, add automated backups (rsync to object storage, database dumps to a separate volume), set up SSL with Let's Encrypt (certbot --nginx), and configure monitoring (Netdata, Prometheus, or even a simple uptime checker).
Keep your system updated weekly with apt upgrade or schedule unattended-upgrades for security patches. Review your firewall rules whenever you add a service—don't leave ports open just because they were needed once.
A clean initial setup saves hours of troubleshooting later. The steps I've shown here are the foundation every production VPS needs before you worry about scaling, caching, or edge deployments.
