From Zero to Production: How I Deployed My App on a VPS Without Losing My Mind
Source: Dev.to
The Premise
Every developer has that moment: you’ve built something you’re proud of, it works on your laptop, your friends are waiting, your users are ready… and then someone asks, “Okay, but how do people actually use it?”
That question sent me down a rabbit hole that lasted an entire day—from buying a VPS at midnight, to staring at a blinking cursor on a remote server, to finally watching my app load in a browser on a live domain with proper https://. This is that story.
Why a VPS?
Think of a VPS (Virtual Private Server) like renting a flat instead of a hotel room.
- Shared hosting (Heroku, Railway, etc.) – the hotel: comfortable, managed, but you share walls with strangers and the rules aren’t yours.
- VPS – the flat: it’s your space, you control everything, but you’re responsible for your own plumbing.
I chose OVHcloud. Their entry‑level VPS plans are competitive on price—especially compared to AWS or DigitalOcean at equivalent specs. I ordered a VPS with Ubuntu 24.04, which landed in my inbox with SSH credentials within minutes.
TIP: When picking a data‑center region, choose the one closest to where most of your users are. Latency is the silent killer of perceived performance. If your users are in Lagos, don’t host in Oregon.
First Login
Once the email with the VPS IP and root credentials arrives, you get a feeling similar to being handed the keys to an empty apartment. You go in, look around, and realize there’s nothing there yet—no furniture, no décor, just your tools and ambition. So the first thing you do is log in.
ssh root@YOUR_VPS_IP
Update the System
apt update && apt upgrade -y
Locking Down the Server
Why?
The moment your server is provisioned, it’s already being targeted by automated bots scanning for open ports. Your server exists on the public internet—and the public internet is not friendly. Before you install a single dependency or write a single config file, you need to lock down who can even talk to your machine. Think of it as building a fence around your new house.
Install & Configure UFW
UFW (Uncomplicated Firewall) is Ubuntu’s friendly wrapper over the more complex iptables firewall rules. The philosophy: deny everything by default, then only open what you need.
# Install UFW
apt install ufw -y
# Deny ALL incoming connections by default
ufw default deny incoming
# Allow all outgoing (your server needs to reach the internet)
ufw default allow outgoing
# Always allow SSH first — if you forget this and enable the firewall,
# you will lock yourself out permanently. Don't be that person.
ufw allow 22/tcp
# Allow HTTP and HTTPS for the web
ufw allow 80/tcp
ufw allow 443/tcp
# Allow port 3000 for Coolify's dashboard
ufw allow 3000/tcp
# Enable the firewall
ufw enable
Check the rules:
ufw status verbose
Analogy: UFW is the security guard at the entrance of your apartment building. He lets in residents (ports you’ve explicitly allowed) and turns away everyone else. Every port you don’t open is a door that doesn’t exist to the outside world.
Add Active Protection with Fail2ban
UFW is passive; it just blocks doors. What about people actively trying to guess their way in? SSH brute‑force attacks are real. Someone, somewhere, is pointing a bot at every IP on the internet and trying admin/admin, root/password, root/123456, etc., thousands of times per minute. Without protection, your server will just sit there and take it.
Fail2ban watches your logs and temporarily bans IPs that fail too many times—like a bouncer who kicks out repeat offenders.
apt install fail2ban -y
Create a local configuration (never edit the default directly—updates will overwrite it):
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
nano /etc/fail2ban/jail.local
Find the [sshd] section and configure it:
[sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 3600
findtime = 600
Enable and start the service:
systemctl enable fail2ban
systemctl start fail2ban
Check who’s been banned:
fail2ban-client status sshd
Analogy: If UFW is the wall around your house, Fail2ban is the doorbell camera that automatically calls the police on anyone who jiggles your handle more than five times.
Your server is now hardened. It has a fence and a bouncer. Nobody who shouldn’t be here is getting in.
Deploying the Application with Coolify
Now comes the fun part—actually putting something on this server. You could go the hard way: manually install Nginx, write config files, manage Docker yourself, wrestle with SSL certs at 2 am. Or you could install Coolify, which does all of that for you through a browser UI.
Coolify is an open‑source, self‑hosted Platform‑as‑a‑Service (PaaS) that abstracts away the low‑level plumbing (Docker, Nginx, SSL, CI/CD) and gives you a clean dashboard to deploy apps, databases, and workers.
(The rest of the guide continues with Coolify installation, app deployment, SSL setup, and post‑deployment tips.)
Coolify – Your Self‑Hosted “Heroku”
Think of Coolify as a Heroku‑ or Railway‑style platform, but you own the server, the data, and you pay the VPS provider directly. It handles:
- Deployments
- SSL certificates
- Environment variables
- Databases
- Reverse‑proxying
All through a clean web UI.
1️⃣ Installation (One‑Liner)
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh
sudo bash install.sh
The installer will:
- Install Docker (Coolify runs everything in containers)
- Set up Coolify’s own containers
- Start the service on port 3000
2️⃣ First Run – Setup Wizard
Open a browser and go to:
http://YOUR_VPS_IP:3000
You’ll see the Coolify setup wizard. Create your admin account and you’re ready to go.
Analogy:
Coolify = buying a plot of land and building your own shopping mall (full control).
Heroku = renting a stall in someone else’s mall (convenient but limited).
3️⃣ Add a Database
Every production app needs a persistent data store.
- Navigate:
Databases → PostgreSQL → New - Choose a version (PostgreSQL 15 or 16)
- Give it a name and click Deploy
Coolify provisions the DB inside Docker. Copy the internal connection string:
postgresql://postgres:your_password@YOUR_VPS_IP:5432/postgres
Why PostgreSQL over SQLite?
SQLite is a file‑based DB – great for development, but the file disappears on redeploy. PostgreSQL is a proper, persistent, concurrent server.
4️⃣ Deploy the Backend (API)
4.1 Create a New Application
- Menu:
Applications → New - Connect your GitHub repository
- Select the backend directory
- Choose Nixpacks as the build pack (auto‑detects Node.js)
4.2 Set Environment Variables
| Variable | Value |
|---|---|
NODE_ENV | production |
DATABASE_URL | postgresql://postgres:your_password@YOUR_VPS_IP:5432/postgres |
JWT_SECRET | a very long random string |
FRONTEND_URL | https://pos.yourdomain.com |
PORT | 3000 |
Generate a strong JWT secret (never commit it to Git):
openssl rand -hex 32
4.3 Assign a Domain & Deploy
- Domain:
https://api.yourdomain.com - Click Deploy
Coolify will:
- Pull the code from GitHub
- Build it with Nixpacks
- Run it in a Docker container
- Expose it via Traefik (built‑in reverse proxy)
- Request a free Let’s Encrypt SSL certificate automatically
When /health returns a response, the API is live.
5️⃣ Deploy the Frontend (React/Vite)
5.1 Create Another Application
Applications → New(same repo)- Select the frontend directory
- Build pack: Static / Vite
5.2 Environment Variables
VITE_API_URL=https://api.yourdomain.com
5.3 Domain & Rewrite Rule
- Domain:
https://pos.yourdomain.com - Static site config – add a rewrite rule so the SPA always serves
index.html:
/* → /index.html
Deploy and watch the logs. When you see “Build successful”, you’re good to go.
6️⃣ DNS – Point Your Domains to the VPS
Your services are still reachable only via the IP address. Add A records at your DNS provider (example uses Namecheap):
| Host | Value (VPS IP) | TTL |
|---|---|---|
api | YOUR_VPS_IP | Automatic |
pos | YOUR_VPS_IP | Automatic |
DNS propagation can take up to 48 hours, but it’s usually resolved within minutes.
7️⃣ Verify Everything
Open a fresh (non‑incognito) tab:
https://pos.yourdomain.com
-
Login page with a padlock → you’re live.
-
Coolify error / 502 → troubleshoot:
- Check backend health:
https://api.yourdomain.com/health - View app logs in Coolify (
App → Logs) - Verify environment variables (especially
DATABASE_URL)
- Check backend health:
Debugging production means reading logs, not
console.log, and each change triggers a redeploy.
8️⃣ Security Extras (UFW & Fail2Ban)
- UFW → configure allowed ports before enabling the firewall.
- Fail2Ban → blocks repeated malicious attempts (its name literally means “fail bots until they’re banned”).
Do the firewall first, then install Fail2Ban; otherwise you’ll lock yourself out.
TL;DR Checklist
- Install Coolify (one command)
- Run the wizard → admin account
- Create PostgreSQL DB → copy connection string
- Deploy backend → set env vars, domain
api.yourdomain.com - Deploy frontend → set
VITE_API_URL, domainpos.yourdomain.com, add rewrite rule - Add DNS A records for
api&pos→ point to VPS IP - Verify SSL (padlock) and health endpoints
- Configure UFW & Fail2Ban for security
You now have a fully self‑hosted, production‑ready platform—your own “Heroku” on a VPS. 🚀
✅ Server Setup & Deployment Checklist
| Step | Description |
|---|---|
| 1 | Bought VPS (OVHcloud) — Ubuntu 24.04 |
| 2 | Updated packages – apt update && apt upgrade |
| 3 | Configured UFW – default deny, opened ports 22, 80, 443, 3000 |
| 4 | Installed Fail2ban – maxretry 5, bantime 1h |
| 5 | Installed Coolify v4 – accessed at :3000 |
| 6 | Provisioned PostgreSQL via Coolify |
| 7 | Deployed backend (Node.js / Nixpacks) with required environment variables |
| 8 | Deployed frontend (React / Vite / Static) with SPA rewrite rule |
| 9 | Configured DNS A records (Namecheap) |
| 10 | Verified SSL via Let’s Encrypt (automatic in Coolify) |
| 11 | Tested live endpoints |
Result: From a blank server to a live app, start‑to‑finish, in one day. It’s not magic — it’s just knowing the order of operations.