<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdulsamad Osunlana</title>
    <description>The latest articles on DEV Community by Abdulsamad Osunlana (@developerbarak).</description>
    <link>https://dev.to/developerbarak</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F630634%2Fdb3f0bd9-29e1-4faf-b3db-c95fa39112d9.jpeg</url>
      <title>DEV Community: Abdulsamad Osunlana</title>
      <link>https://dev.to/developerbarak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/developerbarak"/>
    <language>en</language>
    <item>
      <title>From Zero to Production: How I Deployed My App on a VPS Without Losing My Mind</title>
      <dc:creator>Abdulsamad Osunlana</dc:creator>
      <pubDate>Mon, 02 Mar 2026 01:24:02 +0000</pubDate>
      <link>https://dev.to/developerbarak/from-zero-to-production-how-i-deployed-my-app-on-a-vps-without-losing-my-mind-58bi</link>
      <guid>https://dev.to/developerbarak/from-zero-to-production-how-i-deployed-my-app-on-a-vps-without-losing-my-mind-58bi</guid>
      <description>&lt;h1&gt;
  
  
  From Zero to Production: How I Deployed My App on a VPS Without Losing My Mind
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A brutally honest, step-by-step guide to going from a blank server to a live, production-ready app using OVHcloud, UFW, Fail2ban, and Coolify v4&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Every developer has that moment. You've built something you're proud of. The code works on your laptop. Your friends are waiting. Your users are ready. And then someone asks: &lt;em&gt;"Okay, but how do people actually use it?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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 watching my app finally load in a browser on a live domain with a proper &lt;code&gt;https://&lt;/code&gt;. This is that story.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: Choosing a VPS — The Decision Nobody Tells You About
&lt;/h2&gt;

&lt;p&gt;Before you can deploy anything, you need a place to deploy &lt;em&gt;to&lt;/em&gt;. Think of a VPS (Virtual Private Server) like renting a flat instead of a hotel room. A shared hosting platform (like Heroku or Railway) is the hotel — comfortable, managed, but you're sharing walls with strangers and the rules aren't yours. A VPS is the flat — it's your space, you control everything, but you're responsible for your own plumbing.&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;OVHcloud&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that email lands in your inbox with the VPS IP and root credentials, there's a very specific kind of excitement — like getting handed the keys to an empty apartment. You go in, look around, and realise there's nothing there yet. Nowhere to sit. No furniture. Just your tools and your ambition. So the first thing you do is log in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: Your First Login — Meeting the Empty Room
&lt;/h2&gt;

&lt;p&gt;SSH-ing into a fresh VPS for the first time is humbling. It's just you and a blinking cursor. No apps. No services. Nothing but a bare Ubuntu install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_VPS_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before anything else, update the system. This is like dusting a new flat before you move furniture in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now here's the thing nobody warns you about. The moment your server is provisioned, it's already being target 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 &lt;em&gt;talk&lt;/em&gt; to your machine. That means building a fence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: UFW — Building the Fence Around Your House
&lt;/h2&gt;

&lt;p&gt;Imagine moving into a new house and leaving every door and window wide open because "nobody knows your address yet." That's a fresh server with no firewall. Bots are constantly scanning the internet for open ports — and they don't care how new your server is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UFW (Uncomplicated Firewall)&lt;/strong&gt; is exactly what the name says. It's Ubuntu's friendly wrapper over the more complex &lt;code&gt;iptables&lt;/code&gt; firewall rules.&lt;/p&gt;

&lt;p&gt;The philosophy is: &lt;strong&gt;deny everything by default, then only open what you need.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install UFW&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;ufw &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Deny ALL incoming connections by default&lt;/span&gt;
ufw default deny incoming

&lt;span class="c"&gt;# Allow all outgoing (your server needs to reach the internet)&lt;/span&gt;
ufw default allow outgoing

&lt;span class="c"&gt;# Always allow SSH first — if you forget this and enable the firewall,&lt;/span&gt;
&lt;span class="c"&gt;# you will lock yourself out. Permanently. Don't be that person.&lt;/span&gt;
ufw allow 22/tcp

&lt;span class="c"&gt;# Allow HTTP and HTTPS for the web&lt;/span&gt;
ufw allow 80/tcp
ufw allow 443/tcp

&lt;span class="c"&gt;# Allow port 3000 for Coolify's dashboard&lt;/span&gt;
ufw allow 3000/tcp

&lt;span class="c"&gt;# Enable it&lt;/span&gt;
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check what you've got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The analogy:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But here's the gap UFW leaves open: port 22 — SSH — &lt;em&gt;has&lt;/em&gt; to stay open. That's the door you use to manage your server. And since it has to stay open, anyone in the world can knock on it. UFW will let them knock. What you need is something that notices when someone is knocking &lt;em&gt;too aggressively&lt;/em&gt; and shows them the door permanently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4: Fail2ban — The Bouncer Who Remembers Faces
&lt;/h2&gt;

&lt;p&gt;UFW is great, but it's passive. It just blocks doors. What about the people &lt;em&gt;actively trying to guess their way in&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;SSH brute-force attacks are real. Someone, somewhere, is pointing a bot at every IP on the internet and trying &lt;code&gt;admin/admin&lt;/code&gt;, &lt;code&gt;root/password&lt;/code&gt;, &lt;code&gt;root/123456&lt;/code&gt; thousands of times per minute. Without protection, your server will just sit there and take it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail2ban&lt;/strong&gt; is the answer. It watches your server logs, and when it sees the same IP failing to log in multiple times, it temporarily bans that IP. It's like a bouncer who has a list — three failed attempts and you're cut from the queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a local configuration (never edit the default directly — updates will overwrite it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
nano /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;[sshd]&lt;/code&gt; section and configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ssh&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This says: &lt;em&gt;if any IP fails to authenticate more than 5 times within 10 minutes, ban them for 1 hour.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;fail2ban
systemctl start fail2ban

&lt;span class="c"&gt;# Check who's been banned already (you'd be surprised)&lt;/span&gt;
fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The analogy:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your server is now hardened. It has a fence. It has a bouncer. Nobody who shouldn't be here is getting in. Now comes the fun part — actually putting something &lt;em&gt;on&lt;/em&gt; this server. You could do it the hard way: manually install Nginx, write config files, manage Docker yourself, wrestle with SSL certs at 2am. Or you could install Coolify, which does all of that for you through a browser UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5: Coolify v4 — Your Own Heroku, On Your Own Terms
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coolify&lt;/strong&gt; is an open-source, self-hosted Platform-as-a-Service (PaaS). Think of it as Heroku or Railway — but you own the server, you own the data, and you pay the VPS provider instead of them. It handles deployments, SSL certificates, environment variables, databases, reverse-proxying — all through a clean web UI.&lt;/p&gt;

&lt;p&gt;Installing it is genuinely one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; https://get.coollabs.io/coolify/install.sh &lt;span class="nt"&gt;-O&lt;/span&gt; install.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;bash install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs Docker (Coolify runs everything in containers)&lt;/li&gt;
&lt;li&gt;Sets up Coolify's own containers&lt;/li&gt;
&lt;li&gt;Starts the service on port &lt;strong&gt;3000&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once it's done, open your browser and go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://YOUR_VPS_IP:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the Coolify setup wizard. Create your admin account. That's it — you now have your own cloud platform.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The analogy:&lt;/strong&gt; Coolify is like buying a plot of land and building your own shopping mall, where you can open any number of shops (apps). Heroku is renting a stall in someone else's mall — convenient, but they set the rules and the rent goes up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With your platform ready, the first thing to deploy isn't your app — it's the thing your app depends on. Every application needs somewhere to store its data. And for production, that means a real database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6: PostgreSQL — The Database
&lt;/h2&gt;

&lt;p&gt;In Coolify, navigate to &lt;strong&gt;Databases → PostgreSQL → New&lt;/strong&gt;. Choose a version (PostgreSQL 15 or 16 is fine), give it a name, and deploy it.&lt;/p&gt;

&lt;p&gt;Coolify provisions it inside Docker. The important thing to grab is the &lt;strong&gt;internal connection string&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://postgres:your_password@YOUR_VPS_IP:5432/postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this — your backend will need it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why PostgreSQL over SQLite?&lt;/strong&gt; SQLite is a file. It works brilliantly for development, but a file on a server that gets redeployed gets wiped. PostgreSQL is a proper server — persistent, concurrent, production-grade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Database: running. Now you need the thing that actually &lt;em&gt;uses&lt;/em&gt; it. The backend is the brain of your app — it handles authentication, validates requests, talks to the database, and enforces all the business logic. Without it, the frontend is just a pretty screen with nowhere to send data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 7: Deploying the Backend
&lt;/h2&gt;

&lt;p&gt;You've got a database. Now let's give it something to talk to.&lt;/p&gt;

&lt;p&gt;In Coolify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Applications → New&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Connect your &lt;strong&gt;GitHub repository&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;&lt;code&gt;backend&lt;/code&gt;&lt;/strong&gt; directory&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Nixpacks&lt;/strong&gt; as the build pack (it auto-detects Node.js)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then set your environment variables — these are the secrets your app needs to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NODE_ENV=production
DATABASE_URL=postgresql://postgres:your_password@YOUR_VPS_IP:5432/postgres
JWT_SECRET=a_very_long_random_string_you_generate_once
FRONTEND_URL=https://pos.yourdomain.com
PORT=3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; Your &lt;code&gt;JWT_SECRET&lt;/code&gt; signs every login token. If someone gets it, they can impersonate any user. Generate it with:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;And never commit it to Git. Never.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assign a domain — e.g. &lt;code&gt;https://api.yourdomain.com&lt;/code&gt; — and hit &lt;strong&gt;Deploy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Coolify will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull your code from GitHub&lt;/li&gt;
&lt;li&gt;Build it with Nixpacks&lt;/li&gt;
&lt;li&gt;Start it in a Docker container&lt;/li&gt;
&lt;li&gt;Expose it via Traefik (the reverse proxy it ships with)&lt;/li&gt;
&lt;li&gt;Request an SSL certificate from Let's Encrypt automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the backend is up and you can hit &lt;code&gt;/health&lt;/code&gt; and get a response, the hardest part is done. The API exists. The database is talking to it. All that's left is giving users a face to put to it — the frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 8: Deploying the Frontend
&lt;/h2&gt;

&lt;p&gt;Same process, different directory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Applications → New&lt;/strong&gt; → same GitHub repo&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;&lt;code&gt;frontend&lt;/code&gt;&lt;/strong&gt; directory&lt;/li&gt;
&lt;li&gt;Build pack: &lt;strong&gt;Static / Vite&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_API_URL=https://api.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assign domain: &lt;code&gt;https://pos.yourdomain.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One catch with React SPAs: when a user navigates to &lt;code&gt;https://pos.yourdomain.com/dashboard&lt;/code&gt;, the server looks for a file called &lt;code&gt;/dashboard/index.html&lt;/code&gt; — which doesn't exist. You need to tell the static server to always serve &lt;code&gt;index.html&lt;/code&gt; and let React Router handle the routing.&lt;/p&gt;

&lt;p&gt;In Coolify's static site config, add a rewrite rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* → /index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy it. Watch the build logs. When you see &lt;strong&gt;"Build successful"&lt;/strong&gt; — exhale.&lt;/p&gt;

&lt;p&gt;But here's the thing: your app is still living behind an IP address. &lt;code&gt;https://51.195.xx.xx&lt;/code&gt; is not something you'd put on a business card. You need your domain. You need DNS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 9: DNS — Pointing the World at Your Server
&lt;/h2&gt;

&lt;p&gt;You've got a live backend and frontend. But they're still at IP addresses. You need domains.&lt;/p&gt;

&lt;p&gt;In your DNS provider (we used Namecheap), add &lt;strong&gt;A Records&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YOUR_VPS_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YOUR_VPS_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DNS propagation can take up to 48 hours, but usually resolves within minutes if you're lucky.&lt;/p&gt;

&lt;p&gt;Once DNS resolves, Coolify's Traefik reverse proxy intercepts the request, routes it to the right container, and serves SSL — the padlock in your browser — via Let's Encrypt. Completely free. Completely automated.&lt;/p&gt;

&lt;p&gt;And then you wait. Maybe ten minutes. Maybe two hours if your registrar is slow. But eventually, you type the domain into your browser and — if everything went right — you see your app. Not on localhost. Not on a staging URL. On &lt;em&gt;your&lt;/em&gt; domain. With a padlock.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 10: The Moment of Truth
&lt;/h2&gt;

&lt;p&gt;Open a fresh browser tab — not incognito, not localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://pos.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your login page with a padlock in the address bar — you're live.&lt;/p&gt;

&lt;p&gt;If you see a Coolify error or a 502, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;https://api.yourdomain.com/health&lt;/code&gt; — is the backend responding?&lt;/li&gt;
&lt;li&gt;Coolify logs (click the app → Logs) — what error is it throwing?&lt;/li&gt;
&lt;li&gt;Environment variables — did &lt;code&gt;DATABASE_URL&lt;/code&gt; get set correctly?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Debugging a production deploy is a different discipline from debugging local code — you're reading logs instead of console.logs, and every change means a redeploy. But that's the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fail2ban's name, by the way&lt;/strong&gt; — you were trying to remember it. It comes from exactly what it does: it &lt;em&gt;fails&lt;/em&gt; bots until it &lt;em&gt;bans&lt;/em&gt; them. Simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hardest part wasn't the setup — it was the order.&lt;/strong&gt; Do UFW before Fail2ban. Get ports right before enabling the firewall. Understand what each service needs before restricting network access. Every "locked myself out" story from developers comes from doing things in the wrong order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coolify is worth it.&lt;/strong&gt; The abstraction it provides over raw Docker and Nginx configuration saves hours of work. Its UI is clean, its logs are readable, and auto-SSL is genuinely magical the first time you see it work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Bought VPS (OVHcloud) — Ubuntu 24.04
✅ Updated packages (apt update &amp;amp;&amp;amp; apt upgrade)
✅ Configured UFW — default deny, opened 22, 80, 443, 3000
✅ Installed Fail2ban — maxretry 5, bantime 1h
✅ Installed Coolify v4 — accessed at :3000
✅ Provisioned PostgreSQL via Coolify
✅ Deployed backend (Node.js/Nixpacks) with env vars
✅ Deployed frontend (React/Vite/Static) with SPA rewrite rule
✅ Configured DNS A records (Namecheap)
✅ Verified SSL via Let's Encrypt (automatic in Coolify)
✅ Tested live endpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>beginners</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
