<?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: Yassine Sadgui </title>
    <description>The latest articles on DEV Community by Yassine Sadgui  (@yassines99).</description>
    <link>https://dev.to/yassines99</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064872%2Fcb2b2b45-e62f-470b-bc6a-8219870b814d.png</url>
      <title>DEV Community: Yassine Sadgui </title>
      <link>https://dev.to/yassines99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yassines99"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Trusting "It Works on My Machine" and Actually Learned to Ship</title>
      <dc:creator>Yassine Sadgui </dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:33:07 +0000</pubDate>
      <link>https://dev.to/yassines99/why-i-stopped-trusting-it-works-on-my-machine-and-actually-learned-to-ship-41ml</link>
      <guid>https://dev.to/yassines99/why-i-stopped-trusting-it-works-on-my-machine-and-actually-learned-to-ship-41ml</guid>
      <description>&lt;p&gt;I'll be honest about where this started. I could run &lt;code&gt;docker-compose up&lt;/code&gt; and get an app running locally. I could follow a tutorial and end up with something that "worked." But if you'd asked me to actually explain what happens when a real user hits my app over HTTPS, how traffic gets from the internet to my container, or why my database kept losing data every time I restarted a container, I would have gone quiet.&lt;/p&gt;

&lt;p&gt;That gap bothered me. So I picked a project, the Conduit RealWorld app, and decided I wasn't going to stop until I could deploy it the way an actual production system gets deployed. Not a toy setup. Not "good enough for a demo." A real containerized stack with a reverse proxy, real SSL, and infrastructure that wouldn't fall apart the second I looked away from it.&lt;/p&gt;

&lt;p&gt;This is what that actually looked like, including the parts that broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I was working with
&lt;/h3&gt;

&lt;p&gt;Conduit is a full-stack RealWorld implementation, basically a Medium clone, with a React 19 frontend (built with Vite and SWC), an Express.js 5 backend using Sequelize as the ORM and JWT for auth, and a PostgreSQL 15 database. Perfect project for this, honestly, because it's realistic enough to hit real problems but small enough that I wasn't drowning in complexity before I even got to the infrastructure part.&lt;/p&gt;

&lt;p&gt;The goal was simple to say and harder to do: get this running in Docker, put Nginx in front of it as a reverse proxy, get real SSL working with Let's Encrypt, and make sure the whole thing wouldn't lose data or fall over on a restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose, and why "just Docker" wasn't enough
&lt;/h3&gt;

&lt;p&gt;My first instinct was to containerize each piece separately and wire them together by hand. That lasted about twenty minutes before I gave up and reached for Docker Compose instead.&lt;/p&gt;

&lt;p&gt;Here's the thing nobody tells you clearly enough when you're starting out: the value of Compose isn't just "less typing." It's that your entire infrastructure becomes something you can read. Frontend, backend, database, reverse proxy, all defined in one file, with their relationships to each other spelled out instead of held together by whatever commands you happened to run in whatever order you happened to run them.&lt;/p&gt;

&lt;p&gt;Once I had frontend, backend, and PostgreSQL each in their own service, connected on an internal Docker network, something clicked. I wasn't just running containers anymore. I was describing a system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx: the piece I almost skipped
&lt;/h3&gt;

&lt;p&gt;I'll admit I almost skipped the reverse proxy entirely. My backend already had a port. Why not just expose it directly?&lt;/p&gt;

&lt;p&gt;I'm glad I didn't take that shortcut, because it would have meant learning this lesson the hard way in a much worse context later. Nginx sitting in front of everything meant I had one single entry point handling incoming traffic, and everything behind it, frontend, backend, was invisible to the outside world unless Nginx decided to route it there.&lt;/p&gt;

&lt;p&gt;That single decision solved a handful of problems at once. It gave me a clean place to terminate SSL. It let me route &lt;code&gt;/api&lt;/code&gt; requests to the backend and everything else to the frontend without the frontend needing to know or care that a backend even existed. And it meant if I ever wanted to add caching, rate limiting, or swap out a service entirely, I had one config file to touch instead of rewriting how every piece of the app talked to the internet.&lt;/p&gt;

&lt;p&gt;I also split the setup into two separate Docker networks instead of throwing everything onto one. A proxy-tier network handles Nginx, the SSL companion, and the frontend. A separate internal network handles frontend-to-backend and backend-to-database traffic. The practical effect is that the database is never directly reachable from the proxy tier at all, it simply isn't on that network. If someone compromises the reverse proxy, they still don't have a path straight to the database. Small decision, but it's the kind of small decision that actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SSL part that actually humbled me
&lt;/h3&gt;

&lt;p&gt;This is where things got interesting, and by interesting I mean I broke it more than once.&lt;/p&gt;

&lt;p&gt;Let's Encrypt sounds simple on paper. Run a command, get a certificate, done. I ended up going with the nginx-proxy and acme-companion containers instead of running Certbot by hand, and that decision taught me more than I expected.&lt;/p&gt;

&lt;p&gt;Here's how it actually works: nginx-proxy watches the Docker socket and automatically reconfigures itself whenever containers start, stop, or change, no manual config edits required. The acme-companion container sits next to it, watches for containers with the right environment variables set, and handles requesting and auto-renewing Let's Encrypt certificates on its own. The two talk to each other, and both need access to the Docker socket to do their jobs, which is its own thing to sit with, since that socket is effectively a direct line to controlling Docker itself.&lt;/p&gt;

&lt;p&gt;The part that actually tripped me up wasn't the SSL logic, it was getting the environment variables right so acme-companion knew which domain belonged to which container, and making sure the shared volumes for certs, vhost configs, and the DH param file were mounted correctly across both containers. Get that wrong and you get silent failures, no cert, no clear error telling you why.&lt;/p&gt;

&lt;p&gt;There's a particular kind of satisfaction in watching a padlock icon show up in your browser on something you deployed yourself, on your own server, with certificates you configured by hand instead of a platform doing it silently for you. It's a small thing. It still felt like a real milestone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data loss problem nobody warns you about
&lt;/h3&gt;

&lt;p&gt;Here's a mistake that taught me more than anything else in this project. Early on, I restarted a container during testing and watched all my database data disappear. Just gone.&lt;/p&gt;

&lt;p&gt;Turns out, if you don't explicitly set up persistent storage, your database's data lives inside the container's filesystem, and containers are meant to be disposable. Restart it, rebuild it, replace it, and by default you lose everything that was living inside it.&lt;/p&gt;

&lt;p&gt;The fix is named volumes: telling Docker to store the actual PostgreSQL data outside the container's ephemeral filesystem, in a location that survives no matter what happens to the container itself. Once I wired that up properly, I could tear down and rebuild containers as many times as I wanted, and the data underneath stayed exactly where it was supposed to be.&lt;/p&gt;

&lt;p&gt;It's such a simple concept once you understand it. It's also the kind of thing that will absolutely destroy you in a real production environment if you find out about it the hard way, in front of actual users, instead of alone at your desk during a side project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables and the restart policy I almost forgot
&lt;/h3&gt;

&lt;p&gt;Two smaller things that mattered more than I expected going in.&lt;/p&gt;

&lt;p&gt;First, environment variables. Database credentials, API URLs, secrets, none of that belongs hardcoded into your app or your Dockerfiles. Getting into the habit of pulling all of that out into environment variables, managed through Compose, meant I could change configuration without touching code, and it meant I wasn't staring down the barrel of committing a password to GitHub by accident.&lt;/p&gt;

&lt;p&gt;Second, restart policies. By default, if a container crashes, it just stays down. That's fine on your laptop. It's not fine on a server nobody's actively watching. Setting proper restart policies meant that if something crashed, whether from a bug, a resource spike, or the server itself rebooting, the system would bring itself back up without me needing to SSH in and manually restart things at two in the morning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating database migrations
&lt;/h3&gt;

&lt;p&gt;The last piece was making sure schema changes to the database happened automatically as part of deployment, instead of being a manual step I'd inevitably forget. I built a start.sh entrypoint script for the backend container that runs in a fixed order every time it boots: run migrations, seed the database, then start the actual app. That order matters. Try to start the app before migrations finish and you'll get errors about tables or columns that don't exist yet. Wiring it into the entrypoint like this meant the database structure and the application code stayed in sync automatically, every time, instead of relying on me remembering to run a command I'd definitely forget eventually.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell someone starting this same project
&lt;/h3&gt;

&lt;p&gt;If you're where I was, comfortable running containers locally but shaky on what happens after that, here's what actually mattered:&lt;/p&gt;

&lt;p&gt;Don't skip the reverse proxy, even if it feels like unnecessary complexity for a small project. Understand SSL as a sequence of steps, not a single command. Assume your container data will disappear unless you explicitly tell Docker to keep it somewhere safe. And get comfortable with the idea that things will break in ways tutorials never mention, because tutorials are written by people who already know where the landmines are.&lt;/p&gt;

&lt;p&gt;None of this was hard in the sense of requiring brilliance. It was hard in the sense of requiring patience, and a willingness to actually understand why something broke instead of just copy-pasting a fix until the error went away.&lt;/p&gt;

&lt;p&gt;The repo's up on GitHub if you want to see the actual Compose file, Nginx config, and how everything fits together: &lt;a href="https://github.com/YassineS99/conduit-realworld-example-app" rel="noopener noreferrer"&gt;conduit-realworld-example-app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next up, I want to wire this into an actual CI/CD pipeline so deployment stops being something I do by hand entirely. If you've done that jump yourself, I'd genuinely like to hear what surprised you about it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>nginx</category>
      <category>docker</category>
      <category>postgressql</category>
    </item>
  </channel>
</rss>
