How to Deploy Your App to a VPS: The Guide Nobody Gave You
Your app works. You can see it right there in your browser at localhost:3000. Maybe you built it yourself, maybe you built it with AI, maybe a developer built it for you. Doesn't matter. It works on your computer.
Now you need other people to be able to use it. You need it on the internet, at a real domain, with HTTPS and everything. And this is the exact moment where a terrifying number of projects die.
Not because deployment is impossibly hard. It's not. But because there's a gap between "my app runs on my laptop" and "my app is live on the internet" that nobody explains clearly. Every tutorial assumes you already know half of it. Every AI assistant gives you commands without context. You copy-paste things, something breaks, you don't know why, and you're stuck.
This article is the map. It won't hold your hand through every command — there are linked articles for that — but it will show you the full picture so you know where you are, what's next, and what to do when something goes wrong.
What you actually need
Before you do anything, you need three things:
1. A VPS (Virtual Private Server)
A VPS is a computer in a data center that's always on and always connected to the internet. You rent one from a provider. It runs Linux. You control it remotely.
The main providers are Hetzner, DigitalOcean, Vultr, and Linode. For most apps, the smallest plan is fine — typically $4 to $10 per month. That gets you a server with 1-2 GB of RAM and enough CPU to run a Node.js or Python app without breaking a sweat.
When you sign up, you'll choose an operating system. Pick Ubuntu (the latest LTS version). It has the most tutorials, the most community support, and the fewest surprises. You can use something else, but you'll spend more time translating instructions.
2. A domain name
You need a domain like yourapp.com so people can find your site. You'll buy this from a registrar like Namecheap, Cloudflare, or Google Domains. It's usually $10-15 per year.
You'll point the domain at your VPS by creating a DNS record — specifically, an A record that maps your domain name to your server's IP address. Your VPS provider will show you the IP address when you create the server. Your domain registrar will have a DNS settings page where you enter it.
If DNS is already confusing, read Your Domain Isn't Working: DNS Troubleshooting for Non-Sysadmins after you've gone through the rest of this article.
3. SSH access
SSH is how you connect to your server from your laptop. It's a command-line tool that gives you a terminal on the remote machine, like sitting in front of it. When you create a VPS, the provider will give you either a root password or let you upload an SSH key. Use SSH keys if you can — they're more secure and you won't have to type a password every time.
From your laptop's terminal:
ssh root@your-server-ip-address
If you can run that and get a command prompt on the server, you're in. If it doesn't work, see SSH Connection Refused: How to Get Back Into Your Server.
The steps, in order
Here's what you need to do, at a high level. Each step links to a deeper article when one exists. The order matters — don't skip ahead.
Step 1: Set up the server basics
When you first log into a fresh VPS, you need to do some housekeeping:
- Update the system. Run
apt update && apt upgrade -yto get the latest security patches. - Create a non-root user. Running everything as root is a security risk. Create a regular user, give it sudo access, and use that instead.
- Set up a firewall. Ubuntu has
ufw(Uncomplicated Firewall). At minimum, allow SSH (port 22), HTTP (port 80), and HTTPS (port 443). Block everything else. This takes three commands but prevents a whole category of security problems. - Set up SSH key authentication and disable password login. If you didn't do this during VPS creation, do it now. Password-based SSH login is constantly probed by bots. SSH keys eliminate that entire attack surface.
This step isn't exciting. It feels like busywork when you just want to see your app live. But skipping it means you'll have a server on the internet with no firewall and a password-based root login. That's an invitation for trouble.
Step 2: Install your runtime
Your server needs whatever your app runs on — Node.js, Python, or both. Install them the same way you installed them on your laptop.
For Node.js, use nvm (Node Version Manager). For Python, the system Python is usually fine, but if you need a specific version, use pyenv. If your app uses a database like PostgreSQL or MySQL, install that too.
The key thing: note the exact path to your runtime. Run which node or which python3 after installing and write down what it says. You'll need this later when you set up systemd, and getting it wrong is the #1 reason services fail. More on that in Your App Won't Stay Running: A Guide to Systemd Services.
Step 3: Get your code on the server
The simplest way is git. If your code is on GitHub, GitLab, or Bitbucket:
git clone https://github.com/yourname/yourapp.git
cd yourapp
If your repo is private, you'll need to set up a deploy key or a personal access token on the server. The provider's docs will walk you through it.
Some people use scp or rsync to copy files directly. That works too, but git is better for updates — when you change your code, you just git pull instead of re-uploading everything.
Step 4: Install dependencies and set up environment variables
Run npm install (Node) or pip install -r requirements.txt (Python) to install your app's dependencies on the server.
Then set up your environment variables. Your app almost certainly has them — database URLs, API keys, a NODE_ENV=production flag, a port number. On your laptop these might be in a .env file. On the server, you'll need to either:
- Create a
.envfile on the server (but never commit it to git — it has secrets in it) - Or set the variables directly in your systemd service file (next step)
Missing or wrong environment variables are one of the most common reasons apps crash on the server after working fine locally. If your app is crashing with vague errors, check the env vars first. See Environment Variables Not Working on Your Server for the full breakdown.
Step 5: Run your app with systemd so it stays alive
If you just type node server.js on the server, your app will run — until you close your terminal. Then it dies. You need something that keeps it running in the background, starts it when the server reboots, and restarts it if it crashes.
That something is systemd. You create a small config file that tells the server: "here's my app, here's how to run it, keep it alive."
This is where most people get stuck. The config looks simple, but the details matter: the exact path to Node or Python, the working directory, the user it runs as, the environment variables. Get any one of those wrong and the service fails immediately with an unhelpful error.
This step is important enough that it has its own full article: Your App Won't Stay Running: A Guide to Systemd Services.
Step 6: Put Nginx in front of it
Your app is running on the server, probably on port 3000 or 8000 or something like that. But when someone visits your domain, their browser connects on port 80 (HTTP) or 443 (HTTPS). Something needs to bridge that gap.
That something is Nginx. It sits in front of your app, receives requests from the internet, and forwards them to your app. This is called a "reverse proxy." It also handles SSL, serves static files faster than your app can, and adds a layer of security.
The Nginx config for a basic reverse proxy is about 12 lines. But those 12 lines have to be exactly right — the port number has to match your app, the config file has to be in the right directory, and the headers have to be passed correctly or you'll get weird behavior.
Full guide: Nginx Reverse Proxy: How to Put Your App Behind Nginx. If you hit a 502 Bad Gateway, that's covered in Nginx 502 Bad Gateway: What It Means and How to Fix It.
Step 7: Add SSL with Certbot
Once Nginx is working and you can see your site over HTTP, you need to add HTTPS. Without it, browsers will show a "Not Secure" warning, and you can't handle any kind of sensitive data.
Let's Encrypt provides free SSL certificates. Certbot is the tool that installs and renews them automatically. The whole process is about three commands:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yoursite.com -d www.yoursite.com
sudo certbot renew --dry-run
Certbot modifies your Nginx config to add HTTPS and redirects HTTP traffic to HTTPS. It also sets up automatic renewal since certificates expire every 90 days.
But: Certbot can only work if your domain is already pointing to your server and Nginx is already serving your site on port 80. If DNS isn't set up yet, Certbot will fail with a confusing error. Do things in order.
If your certificate has already expired and you're seeing warnings, see Your SSL Certificate Expired and Your Site Is Showing Security Warnings.
Step 8: Point your domain
If you haven't already, go to your domain registrar and create an A record pointing your domain to your server's IP address. If you want www to work too, create a second A record (or a CNAME) for www.
DNS changes can take anywhere from 5 minutes to 48 hours to propagate, though it's usually under an hour. During this time, some people will see the old site (or nothing) and some will see the new one. This is normal and there's nothing you can do to speed it up.
If things aren't resolving, Your Domain Isn't Working: DNS Troubleshooting for Non-Sysadmins covers the common problems.
What goes wrong (and where to look)
Every step above can fail, and they all fail differently. Here's a quick map:
| Symptom | Likely cause | Where to look |
|---|---|---|
| Can't connect to server at all | SSH misconfigured, firewall blocking, wrong IP | SSH Connection Refused |
| App crashes immediately on server | Missing dependencies, wrong Node/Python version, missing env vars | Environment Variables Not Working |
| App runs manually but dies when you close terminal | Not using systemd (or pm2) | Systemd Services Guide |
| systemd service fails to start | Wrong path, wrong user, missing env vars, wrong directory | Systemd Services Guide |
| 502 Bad Gateway | Nginx is running but your app isn't, or Nginx points to wrong port | Nginx 502 Bad Gateway |
| Domain shows default page or nothing | DNS not pointed, Nginx config not enabled | DNS Troubleshooting |
| "Not Secure" warning in browser | SSL not set up or certificate expired | SSL Certificate Expired |
| "Permission denied" errors | Wrong file ownership or running as wrong user | Linux Permission Denied |
| "Address already in use" | Another process (or a previous crash) is using the port | Address Already in Use |
| Server suddenly slow or crashing | Out of memory or disk full | OOM Killer / Disk Space Full |
Deployment problems almost always come from one of these categories. If you can identify which step is broken, you're 80% of the way to fixing it.
Why "just deploy to Vercel" isn't always the answer
If you've been Googling deployment advice, someone has probably said "just use Vercel" or "just use Railway" or "just use Render." And for certain projects, that's genuinely great advice.
Vercel is excellent for Next.js frontends, static sites, and serverless functions. It handles the entire deployment pipeline and you never touch a server. If your app is a React or Next.js frontend that talks to an external API, Vercel is probably the right choice. Use it and don't overthink it.
But a VPS is what you need when your project doesn't fit the serverless model:
- Custom backends. If you have a Node.js Express API, a Python Flask/Django app, or a FastAPI server that does things beyond simple request-response, a VPS gives you full control.
- Databases. If you want to run your own PostgreSQL or MySQL instead of paying for a managed database, you need a server.
- WebSockets and real-time features. Chat apps, live dashboards, collaborative editing, multiplayer games — anything that keeps a persistent connection open. Serverless platforms either don't support WebSockets or charge a fortune for them.
- Cron jobs and background tasks. Sending scheduled emails, processing queues, running nightly data imports. Serverless functions time out. A VPS doesn't.
- Persistent state. Anything that needs to write to the filesystem, keep a long-running process in memory, or maintain state between requests.
- Cost at scale. A $6/month VPS can handle a surprising amount of traffic. The equivalent on a managed platform might cost $50-100/month once you add a database, background workers, and extra compute.
The managed platforms trade convenience for control. That trade-off is great until you need the control. Then you need a VPS.
Why AI deployment advice is dangerous
This is the part nobody talks about. You can use AI to write your entire app, and it works reasonably well for that. But the moment you ask AI how to deploy that app, the quality drops off a cliff.
AI gives you commands without explaining what they do. It says "run this" and you run it. But you don't know why. When something goes wrong — and it will — you have no idea what any of those commands did, so you can't debug. You're just copy-pasting into a void and hoping.
AI can't see your server. It doesn't know what operating system you're running, what's already installed, what ports are in use, what user you're logged in as, or what other services are running. It guesses. Sometimes it guesses right. Often it doesn't.
AI mixes operating systems. One answer uses Ubuntu commands (apt install), the next uses CentOS commands (yum install). If you're following a multi-step process and the AI switches operating systems mid-conversation, commands start failing and you don't know why. The AI doesn't flag this because it doesn't track what it told you three messages ago.
AI skips security basics. It gets you from "no server" to "app is running" as fast as possible. It usually doesn't mention setting up a firewall, disabling root password login, creating a non-root user, or any of the foundational security steps. Your app is live, but your server is wide open.
AI over-engineers. Ask it how to deploy a simple Node app and it might give you Docker, Docker Compose, a CI/CD pipeline, Kubernetes configuration, and a load balancer. You needed a systemd service and an Nginx config. The AI doesn't know the scale of your project, so it defaults to enterprise-grade complexity for a $6/month VPS running one app.
AI configs are templates, not solutions. Every AI-generated Nginx config uses port 3000. Every systemd service file uses /usr/bin/node. Your app might use port 8000 and your Node binary might be at /home/deploy/.nvm/versions/node/v20.11.0/bin/node. The template doesn't match your reality, and the resulting error messages don't say "wrong port" or "wrong path" — they say "502 Bad Gateway" or "service failed to start" and you're left guessing.
AI is a great tool for writing application code. It's a mediocre tool for deployment because deployment is about your specific server, your specific setup, your specific combination of software. Generating generic text about servers is easy. Making that text work on the server sitting in front of you is where it falls apart.
The overall game plan
If you've read this far, you have the map. Here's the shortest version:
- Get a VPS (Ubuntu, $5-10/month), point your domain at it
- SSH in, do the security basics (firewall, non-root user, SSH keys)
- Install your runtime (Node via nvm, or Python)
- Clone your code, install dependencies, set up env vars
- Create a systemd service so the app stays alive
- Install Nginx and configure a reverse proxy
- Add SSL with Certbot
- Test everything, bookmark your
journalctlcommand for when things break later
Each step is well-documented. Each step has specific things that can go wrong, and those things have specific fixes. The linked articles on this page cover the worst of it.
The gap between "works on localhost" and "works on the internet" is real. But it's not magic. It's about eight concrete steps, done in order, with attention to the details that AI tends to gloss over. You can do this.
Stuck somewhere in the middle?
Deployment is one of those things where you can burn a whole weekend going in circles with AI assistants that can't see your server. MeatButton connects you with a real expert who can look at your actual setup, figure out where things went sideways, and get you live. The first session is free.
Get MeatButton