GUIDE, WITHOUT THE GUESSWORK

PaaS to VPS migration checklist: what Vercel and Railway handled for you

Moving off managed PaaS feels liberating until you realize DNS, SSL, env secrets, deploy rollback, and log routing were all invisible. A step-by-step checklist so nothing falls through the cracks.

PaaS to VPS migration checklist: what Vercel and Railway handled for you

You've hit the bill. Or the cold start. Or the vendor lock that makes multi-region a pricing negotiation. Whatever the trigger, you're moving off Vercel, Railway, Render, or Fly to a VPS you control.

The first deploy works. The second week is where things break — not because the VPS is hard, but because the PaaS was hiding thirty things you didn't know you depended on.

This is the checklist for the things PaaS platforms handle silently that you now own.

Before you touch the VPS

1. Inventory what the PaaS does for you

Open your PaaS dashboard and write down every service it provides. Not what you think it provides — what it actually does. Check:

Write this down. This is your migration scope. Every item on this list is something you need to replace, skip deliberately, or accept you're losing.

2. Export your environment variables

Every PaaS stores env vars differently. Export them now, before you start the migration.

# Railway
railway variables --json > env-export.json

# Vercel
vercel env pull .env.production

# Render
# No CLI export — copy from the dashboard manually

Scan for platform-specific variables. PORT is usually fine. RAILWAY_STATIC_URL, VERCEL_URL, RENDER_INTERNAL_HOSTNAME — these need replacements.

3. Check your DNS situation

If your domain's DNS is managed by the PaaS (Vercel DNS, for example), you need to move DNS management first. This is the step people skip and then wonder why their site goes down for four hours.

The VPS setup checklist

4. Base server hardening

Before deploying your app:

# Create a non-root user
adduser deploy
usermod -aG sudo deploy

# SSH key auth only
cp -r ~/.ssh /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

# Firewall
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Automatic security updates
apt install unattended-upgrades -y
dpkg-reconfigure -plow unattended-upgrades

PaaS platforms do all of this invisibly. On a VPS, skipping this means your server is one brute-force attack away from a bad day.

5. SSL certificates

The PaaS auto-provisioned and auto-renewed your certs. Now you need:

# Install certbot
apt install certbot python3-certbot-nginx -y

# Get certificates
certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Verify auto-renewal
certbot renew --dry-run

The trap: certbot's auto-renewal runs via systemd timer, but nginx doesn't reload automatically after renewal. Add a deploy hook:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
nginx -t && systemctl reload nginx

Without this, your cert renews but nginx keeps serving the old one until the next restart.

6. Reverse proxy

Your app runs on port 3000. The internet expects port 443. nginx bridges the gap:

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

Test with nginx -t before reloading. A syntax error in the config takes down every site on the server, not just yours.

7. Process management

PaaS platforms restart your app when it crashes. On a VPS, you need PM2 or systemd:

# PM2
npm install -g pm2
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup

The pm2 startup command generates a systemd service so PM2 itself restarts after a server reboot. Without it, a reboot means your app stays down until you SSH in and notice.

8. Environment variables

Create a .env file on the server (not in the repo):

# /home/deploy/myapp/.env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://...
# ... rest of your exported vars, minus the platform-specific ones

Permissions matter:

chmod 600 .env
chown deploy:deploy .env

Don't commit .env to git. Don't put it in a world-readable location. PaaS platforms encrypted these at rest — your plaintext file on disk is now the weakest link in your security chain.

9. Deploy pipeline

The PaaS deployed on git push. You need to replace that:

Option A: Git pull + PM2 restart

# deploy.sh
#!/bin/bash
set -e
cd /home/deploy/myapp
git pull origin main
npm ci --production
npm run build
pm2 restart myapp

Option B: GitHub Actions + SSH

Automate it so git push still triggers a deploy, but through your own pipeline.

The key difference from PaaS: if the build fails, you need to catch it. set -e in your deploy script is the minimum. Without it, a failed npm run build still runs pm2 restart, and PM2 restarts your app with stale build output.

10. Rollback plan

PaaS rollback is one click. VPS rollback is whatever you set up:

# Before deploy, tag the current state
git tag pre-deploy-$(date +%Y%m%d-%H%M%S)

# To rollback
git checkout pre-deploy-20260525-143022
npm ci --production
npm run build
pm2 restart myapp

Or use symlinked releases (the Capistrano pattern):

/home/deploy/myapp/
  releases/
    20260525-143022/
    20260525-160000/
  current -> releases/20260525-160000/

Rollback becomes changing a symlink and restarting PM2. No rebuild, no npm install, instant.

11. Logging

PaaS log viewers are gone. Replace them:

# PM2 logs
pm2 logs myapp --lines 100

# Or use journalctl if running as a systemd service
journalctl -u myapp -f

For log retention, PM2's pm2-logrotate module prevents your disk from filling up:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7

12. Health checks and uptime monitoring

The PaaS pinged your app and restarted it on failure. Set up your own:

Don't skip external monitoring. PM2 restarts your app, but it can't tell you when nginx is misconfigured, when your SSL cert expired, or when your server ran out of disk.

After the migration

13. Verify everything

Run through this checklist after your first deploy:

14. Cancel the PaaS

Don't cancel immediately. Keep the PaaS deployment running (but stopped) for at least a week. If something goes wrong on the VPS, you can point DNS back in five minutes instead of rebuilding from scratch.

What you gain, what you lose

You gain: predictable costs, full control, no cold starts, no vendor lock-in, SSH access, the ability to run anything.

You lose: zero-config deploys, automatic scaling, the PaaS team's on-call rotation, and the luxury of not thinking about servers.

That trade is worth it for most apps that have outgrown the free tier. But only if you replace what the PaaS was doing — not just the hosting, but the thirty invisible services around it.