Platforms like Vercel and Netlify make deployment dead simple, but they also make decisions for you. The moment you need a background worker, a custom database setup, or just want to stop paying $20/month for a hobby project, a VPS gives you full control for a fraction of the cost. Knowing how to deploy a web app to a VPS is one of those skills that pays for itself over and over.
This guide goes from a fresh server to a live, SSL-secured application with automated deployments. No vendor lock-in, no platform-specific magic. Just Linux, Nginx, and your code.
TL;DR
- A $5-6/month VPS from DigitalOcean, Hetzner, or Vultr can host multiple web apps.
- The core stack is Nginx (reverse proxy) + PM2 (process manager) + Let’s Encrypt (SSL).
- Security basics take 10 minutes: firewall, SSH keys, non-root user, Fail2ban.
- GitHub Actions gives you push-to-deploy automation without paying for CI/CD.
- If you want VPS control without manual config, tools like Coolify and Dokku add a Heroku-like layer on top.
- When a VPS Beats Platform Hosting
- Choosing a VPS Provider
- Initial Server Setup
- Securing Your Server
- Installing Your Runtime
- Deploying Your Application
- Setting Up Nginx as a Reverse Proxy
- Adding SSL with Let’s Encrypt
- Automating Deploys with GitHub Actions
- Monitoring and Keeping Things Running
- The Shortcut: Self-Hosted PaaS Tools
- Frequently Asked Questions
- Summary
When a VPS Beats Platform Hosting
Platform-as-a-Service tools like Vercel, Netlify, and Railway are great when your app fits their model. Static frontends, serverless functions, and simple APIs deploy in seconds. But the trade-offs show up fast.
A VPS makes more sense when you need persistent background processes (job queues, WebSocket servers, cron tasks), want to run multiple apps on one server instead of paying per-project, need a specific database setup that managed platforms either don’t support or charge extra for, or simply want to understand what’s actually happening between your code and the internet.
Cost is the other big factor. A $6/month DigitalOcean Droplet or a $4.50/month Hetzner cloud server gives you 1-2 vCPUs, 2GB RAM, and 20-40GB storage. That’s enough to run several Node.js apps, a PostgreSQL database, and Nginx simultaneously. The equivalent on a managed platform would cost $20-50/month or more.
If you’re building WordPress sites specifically, managed hosts like Kinsta or Cloudways handle the server management for you. We’ve reviewed both in depth: our Kinsta review covers the premium end, and our Cloudways review covers the flexible middle ground. But for custom web apps, a VPS is the way to go.
Choosing a VPS Provider
Three providers consistently come up in developer communities. Here’s how they compare at the entry-level tier:
| Provider | Starter Price | RAM / CPU | Storage | Standout Feature |
|---|---|---|---|---|
| DigitalOcean | $6/mo | 1GB / 1 vCPU | 25GB SSD | Community tutorials and documentation |
| Hetzner | ~$4.50/mo | 2GB / 2 vCPU | 20GB NVMe | Price-to-performance ratio |
| Vultr | $6/mo | 1GB / 1 vCPU | 25GB SSD | 32 global data center locations |
DigitalOcean is a common recommendation for first-time VPS users. Their documentation library is massive, and their control panel is clean. Hetzner gives you the most resources per dollar, which is why it dominates budget-conscious developer discussions on Reddit. Vultr splits the difference with solid performance and the widest data center coverage.
For this guide, spin up a fresh Ubuntu 24.04 LTS server with at least 1GB of RAM. Every provider above supports it, and the commands from here on are identical regardless of which one you pick.
Initial Server Setup
Once your server is provisioned, SSH into it as root:
ssh root@YOUR_SERVER_IP
First, update everything:
apt update && apt upgrade -y
Create a Non-Root User
Running everything as root is a liability. Create a deploy user with sudo access:
adduser deploy
usermod -aG sudo deploy
Copy your SSH key to the new user so you can log in directly:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Log out and reconnect as the deploy user to verify it works:
ssh deploy@YOUR_SERVER_IP
Securing Your Server
Security on a VPS isn’t optional. An unprotected server will start receiving brute-force SSH attempts within minutes of going online. These three steps take about 10 minutes and block the vast majority of attacks.
Configure the Firewall (UFW)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
This blocks everything except SSH, HTTP, and HTTPS. You can verify the rules with sudo ufw status.
Harden SSH
Edit /etc/ssh/sshd_config and change these lines:
PermitRootLogin no
PasswordAuthentication no
Then restart the SSH service:
sudo systemctl restart sshd
This disables root login entirely and forces key-based authentication. Make sure your deploy user’s SSH key works before enabling this, or you’ll lock yourself out.
Install Fail2ban
Fail2ban monitors login attempts and temporarily bans IPs that fail repeatedly:
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
The default config bans IPs for 10 minutes after 5 failed SSH attempts. That’s solid for most setups. Our fail2ban Nginx guide covers custom jails, WordPress protection, and the recidive jail for repeat offenders.
Installing Your Runtime
Don’t install Node.js from the default Ubuntu repositories. Those packages are typically outdated by multiple major versions. Use NVM (Node Version Manager) instead:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
node --version
For Python apps, Ubuntu 24.04 ships with Python 3.12 pre-installed. Just add pip and venv:
sudo apt install python3-pip python3-venv -y
Install PM2 (Node.js Process Manager)
PM2 keeps your Node.js app running after you close the terminal and restarts it if it crashes. It also handles log rotation:
npm install -g pm2
For Python apps, systemd services or gunicorn + supervisor serve the same purpose. We’ll focus on the Node.js path since it’s the most common for VPS deployments.
Deploying Your Web App to a VPS
Clone your project from GitHub (or whatever Git host you use):
cd /home/deploy
git clone https://github.com/youruser/yourapp.git
cd yourapp
npm install --production
Environment Variables
Never hardcode secrets. Create a .env file in your project directory:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=$(openssl rand -hex 32)
Set proper permissions so only your deploy user can read it:
chmod 600 .env
Start with PM2
pm2 start server.js --name myapp
pm2 save
pm2 startup
The pm2 startup command generates a systemd script that auto-starts your app on server reboot. Run the command it outputs (it’ll look something like sudo env PATH=... pm2 startup systemd -u deploy). After that, your app survives reboots and crashes automatically.
Verify it’s running:
pm2 status
curl http://localhost:3000
Setting Up Nginx as a Reverse Proxy
Your app runs on port 3000, but users expect to hit port 80 (HTTP) and 443 (HTTPS). Nginx sits in front and routes traffic to your app while handling SSL termination and serving static files.
sudo apt install nginx -y
Create a server block configuration:
sudo nano /etc/nginx/sites-available/myapp
Paste this configuration:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
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_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
The proxy_set_header lines handle WebSocket connections and forward the real client IP to your app. Without these, features like rate limiting and IP-based logic won’t work correctly.
⚡ DNS setup: Point your domain’s A record to your server’s IP address. Most DNS changes propagate within 5-30 minutes, though full propagation can take up to 48 hours.
Adding SSL with Let’s Encrypt
No production app should run without HTTPS. Let’s Encrypt provides free SSL certificates, and Certbot automates the entire process including auto-renewal:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot modifies your Nginx config to handle HTTPS and sets up a cron job for automatic renewal. Verify the renewal process works:
sudo certbot renew --dry-run
That’s it. Your app is now live on HTTPS with certificates that renew automatically every 90 days.
Alternative: Cloudflare (Recommended)
If you’d rather skip Certbot entirely, put Cloudflare in front of your server. Point your domain’s nameservers to Cloudflare, set SSL mode to “Full (Strict),” and they handle certificates, renewal, and CDN caching for free. You also get DDoS protection and a web application firewall out of the box.
The setup takes about 5 minutes: create a free Cloudflare account, add your domain, update nameservers at your registrar, and toggle SSL to Full (Strict). Cloudflare issues an origin certificate you install on your server, and all traffic between the browser and your app is encrypted end-to-end. For most VPS deployments, this is the easier path.
Automating Deploys with GitHub Actions
Manually SSH-ing in to run git pull every time you push a change gets old fast. A basic GitHub Actions workflow gives you push-to-deploy for free:
Create .github/workflows/deploy.yml in your repo:
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /home/deploy/yourapp
git pull origin main
npm install --production
pm2 restart myapp
Add your server’s IP and SSH private key as GitHub repository secrets (VPS_HOST and VPS_SSH_KEY). Now every push to main automatically pulls the latest code, installs any new dependencies, and restarts the app.
For a more robust pipeline, you can add a build step, run tests before deploying, and implement zero-downtime restarts with pm2 reload instead of pm2 restart. If you’re deploying a WordPress site specifically, our WordPress GitHub Actions deployment guide covers rsync-based file syncing, WP-CLI maintenance mode, database backups, and staging environments.
Monitoring and Keeping Things Running
Your app is live, but servers need attention. Here’s a minimal monitoring setup that catches problems before your users do.
Check Logs
PM2 handles application logs automatically. View them with:
pm2 logs myapp --lines 50
Nginx stores access and error logs in /var/log/nginx/. These are your first stop when something breaks. The error log especially will tell you about upstream timeouts, configuration issues, and 502 errors.
Uptime Monitoring
You need to know when your app goes down. Uptime Kuma is a free, self-hosted monitoring tool that checks your URLs every few minutes and sends alerts via email, Slack, Discord, or Telegram. It runs on the same VPS as your app in a Docker container.
If you don’t want to self-host monitoring too, services like UptimeRobot and Better Stack have free tiers that monitor up to 50 URLs.
Keeping the System Updated
Set up unattended security updates so your server patches itself:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
This auto-installs security patches without touching your application stack. You still want to manually run apt update && apt upgrade every few weeks for non-security updates.
The Shortcut: Self-Hosted PaaS Tools
If manual Nginx configuration and PM2 management feels like more than you need, a self-hosted PaaS gives you Heroku-like convenience on your own VPS. Three tools stand out:
Coolify is the most popular option right now. It installs on any VPS with a single command, provides a web dashboard for deploying apps from GitHub, auto-provisions SSL, and handles databases (PostgreSQL, MySQL, Redis) through its UI. It uses Docker and Traefik under the hood.
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
Dokku is the original self-hosted PaaS. It takes a different approach: you push code via Git (like Heroku), and Dokku builds and deploys it automatically. It’s lighter than Coolify but doesn’t have a web UI.
CapRover sits between the two. It has a web UI, supports Docker Compose files, and handles custom domains and SSL. It’s been around longer than Coolify and has a stable feature set.
All three are free and open source. The trade-off is that they abstract away the Nginx and Docker details you’d learn from a manual setup. If you’re deploying your first VPS app, going manual at least once teaches you what these tools automate.
If you’ve already done the manual path and want to set up a WordPress site on a VPS specifically, our guide to setting up a WordPress server walks through that exact scenario.
Frequently Asked Questions
How much does a VPS cost for hosting a web app?
Entry-level VPS plans start at $4-6/month and are enough to run several small to medium web apps. Hetzner offers the most resources per dollar at roughly $4.50/month for 2GB RAM and 2 vCPUs. DigitalOcean and Vultr start at $6/month for 1GB RAM.
Is a VPS better than Vercel or Netlify?
It depends on your project. Vercel and Netlify are faster for static sites and serverless functions. A VPS is better when you need persistent processes, custom databases, multiple apps on one server, or full control over your environment. Most developers benefit from knowing both approaches.
Do I need to know Linux to use a VPS?
You need basic command-line skills: navigating directories, editing files, and running commands. This guide covers every command you’ll need. Self-hosted PaaS tools like Coolify can also reduce the amount of Linux knowledge required by providing a web interface.
Can I host multiple apps on one VPS?
Yes. Nginx can route traffic to different apps based on the domain name. Each app runs on a different port internally (3000, 3001, etc.), and Nginx handles the routing. A 2GB RAM server can comfortably run 3-5 small Node.js apps simultaneously.
How do I keep my VPS secure?
The three essentials covered in this guide (UFW firewall, SSH key-only authentication, and Fail2ban) block the majority of automated attacks. Beyond that, keep your system updated with sudo apt update && sudo apt upgrade regularly, and monitor logs with journalctl for anything unusual.
Summary
Deploying to a VPS boils down to six pieces: a server, a user, a firewall, your app, Nginx, and SSL. Once you’ve done it once, the whole process takes about 20 minutes from a blank server to a live, secured application.
The concepts here transfer to any hosting environment. Reverse proxy, process management, SSL termination, security hardening. Whether you later move to Coolify, Docker Compose, or Kubernetes, you’ll recognize every piece.
If managed hosting is more your speed, check out our Cloudways review for a flexible managed VPS option, or our Kinsta review for premium WordPress hosting. Not sure if you even need a VPS yet? Our shared hosting vs VPS comparison covers when shared hosting stops working and when it’s worth the jump. And if you want to take VPS hosting further with WordPress specifically, our WordPress server setup guide picks up right where this one leaves off.