<?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: VidhiDixit2000</title>
    <description>The latest articles on DEV Community by VidhiDixit2000 (@vidhidixit2000).</description>
    <link>https://dev.to/vidhidixit2000</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%2F4096098%2Ffe4c2334-9f0e-4cdb-9d54-7679383f0a7a.png</url>
      <title>DEV Community: VidhiDixit2000</title>
      <link>https://dev.to/vidhidixit2000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vidhidixit2000"/>
    <language>en</language>
    <item>
      <title>Turns out I'm the one making the ghosts💀</title>
      <dc:creator>VidhiDixit2000</dc:creator>
      <pubDate>Sat, 31 Oct 2026 22:43:09 +0000</pubDate>
      <link>https://dev.to/vidhidixit2000/turns-out-im-the-one-making-the-ghosts-3oij</link>
      <guid>https://dev.to/vidhidixit2000/turns-out-im-the-one-making-the-ghosts-3oij</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;On 8 August I committed a file called &lt;code&gt;seed.sql&lt;/code&gt;. On 25 October I filled it in.&lt;/p&gt;

&lt;p&gt;For 78 days it was zero bytes. Nothing ever told me🙃. The file existed. The import ran. The app worked. Only the data was missing — and an empty file doesn't raise an error. It just sits there being nothing, very quietly, for two and a half months.&lt;/p&gt;

&lt;p&gt;I found out while deploying, a few days ago. So I built something to go looking for whatever else was in there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tcgvtc0111hef7255s5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tcgvtc0111hef7255s5.gif" alt="hard work cat" width="220" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ghosts in the Machine&lt;/strong&gt; reads a public repository's git history and finds failures that never produced an error — the ones nobody noticed, because there was nothing to notice.&lt;/p&gt;

&lt;p&gt;Point it at a repo. It walks the commits, finds what broke silently, and an open-weight model groups it, ranks it, and writes up what happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   a public repo
        │
        ▼
 ┌─────────────────┐
 │  walk history   │   every commit, every diff
 └────────┬────────┘
          ▼
 ┌─────────────────┐
 │  rules          │   no AI. zero-byte blobs,
 │  (deterministic)│   credential-shaped lines
 └────────┬────────┘
          ▼
 ┌─────────────────┐
 │  oddness        │   no AI. size collapses, resurrected
 │  (deterministic)│   paths, diffs that don't match
 └────────┬────────┘   their own message
          ▼  findings, with commit hashes
 ┌─────────────────┐
 │  Gemma          │   did this break? which of these
 │  (open weights) │   matter? how do I explain it?
 └────────┬────────┘
          ▼
 ┌─────────────────────────────────────┐
 │  what the model made of it          │
 ├─────────────────────────────────────┤
 │  what git actually recorded         │  ← both shown, so you can
 └─────────────────────────────────────┘    check one against the other
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran it on my own repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seed.sql&lt;/code&gt; — committed empty, filled 78 days later&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; — committed empty in the initial commit, filled 79 days later&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;readme.md&lt;/code&gt; — committed empty, by a commit whose message was &lt;strong&gt;"Add README"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A database password in three places in one commit, one of them commented out&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;manifest.json&lt;/code&gt;, empty, which I later deleted without ever noticing it held nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is mine. None of them ever failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I read the theme??
&lt;/h3&gt;

&lt;p&gt;The organisers suggested a haunted terminal and an offline oral history tool. I went elsewhere. I kept coming back to what a ghost actually is: something present, that nobody can see, that everyone has stopped looking for.&lt;/p&gt;

&lt;p&gt;That describes a file committed empty exactly. It's in the tree. It's in every clone. It's in the history permanently. And because it never errors, nobody is looking.&lt;/p&gt;

&lt;p&gt;The repository is the haunted house🏚️. The commits are the record of who lived there🧌🧟‍♀️. I just read the record.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjmq46x9bzotkijge6ez.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjmq46x9bzotkijge6ez.gif" alt="ghost in haunted house gif" width="220" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Who this is for
&lt;/h3&gt;

&lt;p&gt;The case I kept thinking about isn't mine. It's inheriting a repo.&lt;/p&gt;

&lt;p&gt;You join a team, or pick up something abandoned, and you want to know: what's wrong with this that nobody is going to tell me?😟 The README is aspirational. The code is current. The history holds the real answers, and nobody reads 500 commits to find them.&lt;/p&gt;

&lt;p&gt;Nobody hands over a project with a list of everything quietly broken in it. Partly because it would be awkward. Mostly because they don't know either. That's the whole point — these failures produced no error, so the person who made them is as unaware as the person inheriting them.&lt;/p&gt;

&lt;p&gt;The same goes for open-sourcing something, or handing a repo to a client. What's in the history that shouldn't be? Secrets survive deletion, and people usually find that out afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo▶️
&lt;/h2&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://ghostsinthemachine.tech" rel="noopener noreferrer"&gt;ghostsinthemachine.tech&lt;/a&gt;&lt;/strong&gt; · also at &lt;strong&gt;&lt;a href="https://ghosts-in-the-machine.onrender.com/" rel="noopener noreferrer"&gt;ghosts-in-the-machine.onrender.com&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;iframe src="https://player.mux.com/watFp00ddTeBrF2542BhvG02PPpKs49N2blzffWd00KI4U" width="710" height="399"&gt;
&lt;/iframe&gt;

&lt;/p&gt;

&lt;p&gt;Two exhibits load on arrival, from my own repositories. Paste in any public repo and it will read that one instead.&lt;/p&gt;

&lt;p&gt;It's a free instance, so the first request after a quiet spell takes a moment to wake up👐🏻. Small and medium repos come back in under a minute. Large ones take longer, because cloning dominates🥲.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwue93nlpzvpacdy9gclv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwue93nlpzvpacdy9gclv.png" alt="screenshot of the page" width="525" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://github.com/VidhiDixit2000/ghosts-in-the-machine" rel="noopener noreferrer"&gt;github.com/VidhiDixit2000/ghosts-in-the-machine&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It?
&lt;/h2&gt;

&lt;p&gt;Two layers, deliberately separated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deterministic layer&lt;/strong&gt; walks the history and emits findings. No🙅🏻‍♀️ AI. It checks whether a blob is zero bytes when the file extension implies content🔎, and whether added lines look like credentials🔎. Facts, with commit hashes attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model layer&lt;/strong&gt; answers a different question: which of these matter, which are the same story, and how do I explain them to a person? That's Gemma, via OpenRouter — open weights, hosted inference, so the demo runs without a GPU.&lt;/p&gt;

&lt;p&gt;The split isn't tidiness. Each layer does what the other is bad at.&lt;/p&gt;

&lt;p&gt;A regular expression can't tell you an empty file mattered. It has no idea whether 78 days is a long time. Ask a model to scan a whole history instead and it invents findings, misses real ones, and burns tokens doing it.&lt;/p&gt;

&lt;p&gt;Here's the split earning its keep. One repo produced three separate findings: a connection string in &lt;code&gt;database.py&lt;/code&gt;, a &lt;code&gt;DATABASE_URL&lt;/code&gt; in &lt;code&gt;docker-compose.yml&lt;/code&gt;, and a &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; in the same file. &lt;br&gt;
Three true findings. One story — the same password, three places, one commit. The rules found them. Only the model saw they were one ghost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjgm17a6yib7bpmbstk9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjgm17a6yib7bpmbstk9.png" alt="single narration for 3 similar findings screenshot" width="455" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the page, both are shown. &lt;strong&gt;What the model made of it&lt;/strong&gt; sits above &lt;strong&gt;what git actually recorded&lt;/strong&gt;, so you read the narration and then check it against the hashes underneath.&lt;/p&gt;

&lt;p&gt;That mattered more than I expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model was confidently wrong, and I could prove it😏
&lt;/h3&gt;

&lt;p&gt;While I was tuning⛏️ the narration prompt, Gemma 3 12B wrote this about the credentials:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These values... were removed from the working tree &lt;strong&gt;before the commit&lt;/strong&gt;.&lt;br&gt;
However, they remain present in the repository's history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's impossible!! Git history contains what was committed. If something was removed before a commit, that commit never recorded it — it wouldn't be in the history at all. The sentence contradicts itself🙃.&lt;/p&gt;

&lt;p&gt;This was &lt;code&gt;google/gemma-3-12b-it&lt;/code&gt; at temperature 0.3.&lt;/p&gt;

&lt;p&gt;What actually happened: committed 30 July, removed 25 October. Nearly three months apart. The findings said so correctly. The model garbled them on the way to prose.&lt;/p&gt;

&lt;p&gt;I only noticed because the evidence was sitting right underneath. If the narration had replaced the findings instead of sitting beside them, I'd have shipped a well-written falsehood about my own repository.&lt;/p&gt;

&lt;p&gt;Moving to Gemma 3 27B and lowering the temperature🎨🌡️ fixed that particular error. But the architecture caught it, not the model choice.&lt;/p&gt;

&lt;p&gt;If you're building on a model: test across versions &lt;em&gt;and&lt;/em&gt; temperatures. A wrong answer at 0.3 looks completely reasonable until you check it against something that can't be wrong😂.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mistake I made, and what I did about it👇🏻
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcf9hiy2abtce4ex52b7m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcf9hiy2abtce4ex52b7m.gif" alt="mistakes gif" width="220" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I started by writing rules for failures I'd personally hit. Empty files, because of &lt;code&gt;seed.sql&lt;/code&gt;. Leaked credentials, because of a &lt;code&gt;.dockerignore&lt;/code&gt; I forgot and a password I thought I'd deleted by commenting it out.&lt;/p&gt;

&lt;p&gt;Then I saw what I'd built: a tool that finds failures someone has already met. For a project about &lt;em&gt;failures nobody noticed&lt;/em&gt;, that's close to self-defeating. A rule per failure type, forever♾️, and only ever the ones already on the list.&lt;/p&gt;

&lt;p&gt;The way out isn't more rules. There are infinitely many. It's changing what the rules are &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Git's vocabulary is tiny🤏🏻. A file appeared, vanished, changed, or moved. Its bytes grew or shrank. That's nearly all of it. So however many ways software can break, a failure has to express itself in that vocabulary to be visible at all — and it lands in one of a few shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;something got smaller when it shouldn't have&lt;/li&gt;
&lt;li&gt;something vanished and came back&lt;/li&gt;
&lt;li&gt;two paths that should have been one&lt;/li&gt;
&lt;li&gt;a diff that doesn't match what its message claims&lt;/li&gt;
&lt;li&gt;a change that undid an earlier change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Truncated JSON and a gutted &lt;code&gt;requirements.txt&lt;/code&gt; are different failures with the same shape. A case-only rename and a resurrected file are different failures with the same shape.&lt;/p&gt;

&lt;p&gt;So I added a second deterministic layer that names nothing. Six measurements: size collapse, a file too small for its extension, a resurrected path, two paths differing only in case, a large diff under a short message, a change that's almost all deletion. Each scores how &lt;em&gt;unusual&lt;/em&gt; a commit is. The dozen oddest go to the model in one call, and the model says what — if anything — went wrong.&lt;/p&gt;

&lt;p&gt;Like a metal detector. It doesn't know what a coin is. It knows there's metal here. Someone else digs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The signal set is bounded by what git can record. What the model can name isn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here it is working. On &lt;a href="https://github.com/andela/buggy-python" rel="noopener noreferrer"&gt;andela/buggy-python&lt;/a&gt;,&lt;br&gt;
three commits long, the oddness layer flagged a &lt;code&gt;.json&lt;/code&gt; file deleted inside a 14,000-line commit. Neither rule would have caught it — not empty, not a credential. It surfaced because it was &lt;em&gt;strange&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And then the model explained it as an attempt to hide evidence.&lt;/p&gt;

&lt;p&gt;Both halves of that sentence are the point.&lt;/p&gt;

&lt;p&gt;It also found something in my own repository that I'd never have written a rule for🫢. My README describes GitHub Actions. No commit in the history implements them. The measurements only noticed that a large change arrived under a short commit message — they have no concept of documentation drift. The model read the change and named it🥹.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewcz75fytg3jcihk9vrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewcz75fytg3jcihk9vrh.png" alt="buggy-code readme evidence screenshot" width="627" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The narration varies between runs. A later run described the same two commits as a formatting change instead. The findings underneath don't move.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To be honest about it: those six measurements are still rules. The difference is how much each one buys.&lt;/p&gt;

&lt;p&gt;My first rule — the one that looks for empty files — finds empty files. That is all it will ever find. But "this file lost most of its contents" isn't tied to any failure. It catches a truncated JSON, a gutted &lt;code&gt;requirements.txt&lt;/code&gt;, a half-saved migration, a bad merge. One measurement, many failures, including ones I've never seen.&lt;/p&gt;

&lt;p&gt;The rules didn't go away. They stopped growing every time software finds a new way to break quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This tool reads your repository.&lt;/strong&gt; Every commit, every diff, every line that ever held a credential. If it's going to do that, you should be able to read it back: what it looks for, what it sends to a model, what it redacts before sending. The code is public for the same reason the evidence sits beside the narration. A tool making claims about your history should be checkable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open weights meant I could reason about failure.&lt;/strong&gt; When Gemma produced that impossible sentence, I could see which model, which version, what temperature, what prompt. I changed two of them and watched the failure go away. That's a debugging loop. Behind an opaque API it would have been a shrug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Doesn't Do 🙅🏻‍♀️
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two named rules, six measurements.&lt;/strong&gt; Most well-maintained repositories come back empty, correctly. The page says so rather than pretending otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It doesn't read a whole repository.&lt;/strong&gt; Commit depth is capped, and the page tells you when a history was truncated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large repos are slow.&lt;/strong&gt; Cloning dominates, not scanning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The model reads intent into odd commits, until you stop it.&lt;/strong&gt; On a repo called &lt;code&gt;buggy-python&lt;/code&gt;, with a commit message reading "write buggy code", it described a deleted JSON file as an attempt to hide evidence. The measurement was right that something unusual happened. Everything after that was the model writing a story. Telling it not to speculate removed the story. Telling it not to invent sequence did not — until I stopped banning the words I didn't want and prescribed the ones I did.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The narration isn't stable.&lt;/strong&gt; Same repo, same prompt, same temperature: one run called a deletion "possibly intentional experimentation", the next called it "a deliberate attempt to hide evidence". No code changed in between.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It still summarises badly.&lt;/strong&gt; I could stop it inventing things. I couldn't stop it garbling them. In one run it wrote that a file was "added and deleted in the same commit" — and then, in the very next sentence, said the file had existed in an earlier commit. Both can't be true. The findings it was given said the right thing: added in one commit, deleted in a later one. The model had the facts and mangled them while squeezing them into two short paragraphs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secret detection is shallow&lt;/strong&gt; next to dedicated tools. The quiet-failure half is where the gap is.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is visible on the page, because the evidence sits under the narration. That isn't a coincidence. It's why the page is built that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It💪🏻🔥
&lt;/h2&gt;

&lt;p&gt;I'm the kind of person who commits functionality in pieces. Somewhere in all that pushing, something ends up in a file that shouldn't be there. There are mistakes in every repo I've shipped that I'd only see if I stepped away, came back, looked properly, and went &lt;em&gt;wait, what?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built something to look. It spent most of its time looking at mine.&lt;/p&gt;

&lt;p&gt;The one that got me wasn't the empty files. It was the password. I'd found it weeks earlier — a connection string in &lt;code&gt;database.py&lt;/code&gt; — and I commented it out, felt responsible, moved on. My own tool later showed me that same line, in a commit from July, with the &lt;code&gt;#&lt;/code&gt; still on the front. The comment character meant nothing. Git had kept it the whole time, and so had every clone anyone had ever made.&lt;/p&gt;

&lt;p&gt;Then the same password twice more, in a file I hadn't thought about. Three copies of one mistake, and I'd only ever found one.&lt;/p&gt;

&lt;p&gt;Somewhere in there it stopped feeling like I was finding ghosts and started feeling like I'd been making them🙃😂.&lt;/p&gt;

&lt;p&gt;The 78-day empty file is the one I tell people about, but it isn't really a mistake. Nobody did anything wrong. A file was committed before it had content, and there was no moment at which anything would have said so. It sat there, in every clone, for two and a half months, being nothing.&lt;/p&gt;

&lt;p&gt;That's what I find unsettling about git. Your repository remembers everything you did and tells you none of it unless you go and ask. Every project I've shipped has things in it I don't know about yet.&lt;/p&gt;

&lt;p&gt;Which means the next person to open one of my repos wouldn't know either. That part I hadn't thought about until I was looking at the output.&lt;/p&gt;

&lt;p&gt;Building a ghost detector and finding five of your own is a strange way to spend a couple of days. I commit differently now😂.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0i7v4w2eoyg19f83k4u7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0i7v4w2eoyg19f83k4u7.gif" alt="changed gif" width="220" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories🏅
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3 27B&lt;/a&gt; (open weights), served through OpenRouter, does all the judging, grouping and narration. The project does not work without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt; is the runtime: the web app and every inference call run from a single auto-deploying service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — &lt;a href="https://elevenlabs.io" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt; generates the spoken introduction and the ambient room tone on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of a .tech Domain&lt;/strong&gt; — live at &lt;a href="https://ghostsinthemachine.tech" rel="noopener noreferrer"&gt;ghostsinthemachine.tech&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of GitHub Copilot&lt;/strong&gt; — a &lt;a href="https://github.com/VidhiDixit2000/ghosts-in-the-machine/blob/main/.github/workflows/self-scan.yml" rel="noopener noreferrer"&gt;GitHub Actions workflow&lt;/a&gt; runs the tool against its own repository on every push, so the project is continuously scanned by itself. &lt;a href="https://github.com/VidhiDixit2000/ghosts-in-the-machine/actions" rel="noopener noreferrer"&gt;Recent runs.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is &lt;a href="https://www.python.org" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, &lt;a href="https://fastapi.tiangolo.com" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; and &lt;a href="https://gitpython.readthedocs.io" rel="noopener noreferrer"&gt;GitPython&lt;/a&gt;: history walking, the rules, the oddness measurements and the web app.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>I followed Keploy's quickstart as a first-time user. Here's where it broke.</title>
      <dc:creator>VidhiDixit2000</dc:creator>
      <pubDate>Tue, 27 Oct 2026 06:23:27 +0000</pubDate>
      <link>https://dev.to/vidhidixit2000/i-followed-keploys-quickstart-as-a-first-time-user-heres-where-it-broke-1c3i</link>
      <guid>https://dev.to/vidhidixit2000/i-followed-keploys-quickstart-as-a-first-time-user-heres-where-it-broke-1c3i</guid>
      <description>&lt;p&gt;A couple of months ago I came across a product called &lt;a href="https://keploy.io" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; on LinkedIn. I went to their site to read about it, and the pitch stopped me: it writes your integration tests for you by watching your app run.&lt;br&gt;
I went like --&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nde627n7ducciv598ai.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nde627n7ducciv598ai.gif" alt="impressive image gif" width="500" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it did that by Not generating them from your code. Watching actual traffic. You run your app, use it normally, and Keploy records what came in and what went out. Later it replays those requests against your app and checks it still responds the same way.&lt;/p&gt;

&lt;p&gt;I wanted to see whether that held up, &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ur9g2c62bovwk3mta6d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ur9g2c62bovwk3mta6d.gif" alt="curious/mischevious image gif" width="220" height="131"&gt;&lt;/a&gt;&lt;br&gt;
so I ran one of their Go quickstarts end to end on Windows through WSL. This is what it was like.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Integration tests usually mean keeping a database alive, writing mocks by hand, and updating those mocks every time a schema changes. It's a lot of upkeep for tests that mostly verify things you already know work.&lt;/p&gt;

&lt;p&gt;Keploy's approach is to record once and replay forever. It supports several languages — I used one of their &lt;a href="https://keploy.io/docs/quickstart/samples-echo/" rel="noopener noreferrer"&gt;Go quickstarts&lt;/a&gt; because that's what I wanted to try.&lt;/p&gt;

&lt;p&gt;When you start a recording, Keploy moves your app to another port and takes the original one for itself, so every request passes through it. At the same time, an eBPF probe watches the syscalls your app makes to the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5m7e3nowq3wh1w5l1d9o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5m7e3nowq3wh1w5l1d9o.gif" alt="hide and seek image gif" width="220" height="156"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECORD
  your curl  -&amp;gt;  Keploy (:8082)  -&amp;gt;  app (:35555)  -&amp;gt;  Postgres
                      |                                   |
                  test case                             mock

REPLAY
  Keploy  -&amp;gt;  replays recorded request  -&amp;gt;  app  -X-  Postgres
                                             |
                                     answered from mocks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So one recording run captures two things: the HTTP exchange becomes a &lt;strong&gt;test case&lt;/strong&gt;, and the database exchange becomes a &lt;strong&gt;mock&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On replay, your app starts and runs for real — your router matches the path, your handler executes, your code does its work. Only the database calls are faked, answered from the recording. Postgres never gets touched.&lt;/p&gt;

&lt;p&gt;Because the probe sits outside your code rather than inside it, you don't import a library or change a line of your app. That's also why Keploy needs &lt;code&gt;sudo&lt;/code&gt;, and why it only runs on Linux — hence WSL on Windows.&lt;/p&gt;



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

&lt;p&gt;Their &lt;a href="https://keploy.io/docs/quickstart/samples-echo/" rel="noopener noreferrer"&gt;quickstart docs&lt;/a&gt; offer several Go samples. I picked &lt;strong&gt;Echo + Postgres&lt;/strong&gt; — a &lt;a href="https://github.com/keploy/samples-go/tree/main/echo-sql" rel="noopener noreferrer"&gt;URL shortener&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The setup is: install the CLI, start Postgres in Docker, build the app, and run it under &lt;code&gt;keploy record&lt;/code&gt;. Then you just use the API. I made two curl calls — a POST creating a short URL, and a GET following it. Both became test cases, written to &lt;code&gt;keploy/test-set-1/&lt;/code&gt;, with the Postgres exchanges saved alongside them as mocks.&lt;/p&gt;

&lt;p&gt;A few small things didn't match as I went. The docs site, its screenshots, and the binary I installed were on three different versions. The README pointed at a line number that had moved — and the quickstart pointed at a different wrong one. A flag the docs use had been removed. The quickstart skips a login step that everything after it depends on, so the first failure is a 401 timeout that never mentions authentication. None of these are dramatic on their own, but following instructions literally means hitting each one and stopping to work out why.&lt;/p&gt;

&lt;p&gt;Then I hit a real one.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudsyz8d90eu9sqtl2jo0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudsyz8d90eu9sqtl2jo0.gif" alt="sad and shocked gif" width="220" height="391"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What I'd fixed first, and what actually broke
&lt;/h2&gt;

&lt;p&gt;I ran the test command straight from the docs. Keploy started my app, and then the run died.&lt;/p&gt;

&lt;p&gt;The errors talked about the app being unreachable and the database connection failing. So I went looking at Postgres, at ports, at the binary — all of it fine.&lt;/p&gt;

&lt;p&gt;The real cause was a folder I'd never opened. The sample repo ships with a &lt;code&gt;test-set-0&lt;/code&gt; already committed, recorded by someone else a long time ago. My own recording had created &lt;code&gt;test-set-1&lt;/code&gt;. Without naming a test set, Keploy picked up both and started with theirs.&lt;/p&gt;

&lt;p&gt;Its mocks were in an older format that the current version can't read — eighteen &lt;code&gt;failed to unmarshal&lt;/code&gt; errors. With no mocks loaded, nothing answered my app's startup database ping, so the app exited. And with the app gone, the whole run collapsed. My two tests were never reached, let alone run.&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="nt"&gt;--test-sets&lt;/span&gt; test-set-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One flag, and it skips theirs entirely.&lt;/p&gt;

&lt;p&gt;Then a smaller oddity: with that flag everything passed, but the summary still reported "across 2 test-set(s)" and threw a red error about a missing report for &lt;code&gt;test-set-0&lt;/code&gt; — a set that never ran. The filter controls which tests execute, but not what gets counted.&lt;/p&gt;



&lt;h2&gt;
  
  
  What convinced me
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrtbxto0v40opavj2fqu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrtbxto0v40opavj2fqu.gif" alt="here you go image gif" width="600" height="337"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPLETE TESTRUN SUMMARY.
      Total tests: 2
      Total test passed: 2
      Total test failed: 0
      Total time taken: "10.09 s"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And higher up in the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"testsRan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mocksConsumed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four mocks consumed. My app pinged the database, created its table, and ran both queries — and Postgres answered none of it. Every one of those calls came from the recording.&lt;/p&gt;

&lt;p&gt;That's the whole idea in a single line. Real code running, real database work happening, no database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits
&lt;/h2&gt;

&lt;p&gt;Keploy is most useful when you already have a working service and want regression coverage without maintaining a pile of database mocks by hand. The recorded traffic gives you a practical starting point — but it doesn't replace deliberately testing paths you haven't exercised. If you never send a request through a particular part of your app, Keploy can't record a test for it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjskngx8nhdfd8oh01wx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjskngx8nhdfd8oh01wx.gif" alt="here you go image gif" width="220" height="124"&gt;&lt;/a&gt;&lt;br&gt;
I wrote up the full walkthrough &lt;a href="https://keploy-go-tutorial.vercel.app" rel="noopener noreferrer"&gt;here&lt;/a&gt; — every version mismatch and workaround, in the order you'll hit them. If you're going down the same path, it should save you some time.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>devops</category>
      <category>wsl</category>
    </item>
    <item>
      <title>My app could read the database. pgAdmin couldn't. Both were pointing at "localhost:5432"</title>
      <dc:creator>VidhiDixit2000</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:04:06 +0000</pubDate>
      <link>https://dev.to/vidhidixit2000/my-app-could-read-the-database-pgadmin-couldnt-both-were-pointing-at-localhost5432-59bo</link>
      <guid>https://dev.to/vidhidixit2000/my-app-could-read-the-database-pgadmin-couldnt-both-were-pointing-at-localhost5432-59bo</guid>
      <description>&lt;p&gt;I was running a FastAPI app in Docker with a PostgreSQL container next to it. The app worked. It wrote rows, it read them back, everything behaved.&lt;/p&gt;

&lt;p&gt;The database itself, &lt;code&gt;fastapi-db&lt;/code&gt;, was one I'd created inside the dockerised PostgreSQL — the container defined in my compose file. My app had created the &lt;code&gt;products&lt;/code&gt; table in it and was happily writing rows.&lt;/p&gt;

&lt;p&gt;Then I opened pgAdmin to look at that data directly, connected to &lt;code&gt;localhost:5432&lt;/code&gt;, and found nothing. No &lt;code&gt;fastapi-db&lt;/code&gt;. No &lt;code&gt;products&lt;/code&gt; table. Just &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;template0&lt;/code&gt;, &lt;code&gt;template1&lt;/code&gt; — the default databases of an empty install.&lt;/p&gt;

&lt;p&gt;So my application was clearly talking to a database. pgAdmin was clearly talking to a database. They weren't the same database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I assumed first&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first thought was that pgAdmin wasn't refreshing, or that Docker wasn't persisting anything. Both wrong, and both cost me time.&lt;/p&gt;

&lt;p&gt;The thing that actually settled it was running this in pgAdmin's query tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It came back with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL 18.4 on x86_64-windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows. Not the Linux build that would be running inside a container. pgAdmin was connected to a PostgreSQL I'd installed on Windows months earlier and forgotten about — one that was running as a background service the whole time.&lt;/p&gt;

&lt;p&gt;To confirm who owned the port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;netstat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ano&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findstr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;5432&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the PID holding the port, and it belonged to the Windows PostgreSQL service. It had claimed &lt;code&gt;localhost:5432&lt;/code&gt; at boot, long before Docker was involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part I'd misunderstood&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what I hadn't grasped: my app and pgAdmin were reaching the database over two completely different routes.&lt;/p&gt;

&lt;p&gt;Inside Docker Compose, containers reach each other by service name on the Docker network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastapi-container  ───►  postgres-db:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That never touches the host machine's ports at all. It's container-to-container, resolved by Docker's internal DNS. Which is why my app worked perfectly and gave me no hint anything was wrong.&lt;/p&gt;

&lt;p&gt;pgAdmin runs on Windows, outside Docker. It can only reach the container through a published port — the host-side mapping in the compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;localhost:5432  ───►  (published port)  ───► container:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;localhost:5432&lt;/code&gt; on my machine was already spoken for.&lt;/p&gt;

&lt;p&gt;So one path was working fine and the other was quietly landing somewhere else entirely. Same port number in both places, two unrelated destinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Change the host side of the mapping so it doesn't collide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5433:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two numbers do different jobs, and this is the bit worth internalising. The left one is the port on your machine. The right one is the port inside the container. Postgres inside the container is still listening on 5432 and doesn't need to change — only the door on the outside moves.&lt;/p&gt;

&lt;p&gt;Then a new pgAdmin connection to &lt;code&gt;localhost:5433&lt;/code&gt;, and &lt;code&gt;fastapi-db&lt;/code&gt; appeared immediately — the database I'd created inside the container, with the &lt;code&gt;products&lt;/code&gt; table and the same rows my app had been writing all along.&lt;/p&gt;

&lt;p&gt;My FastAPI config didn't change at all. It was still using &lt;code&gt;postgres-db:5432&lt;/code&gt;, because it was never going through the host in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A smaller thing that had also confused me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pgAdmin's sidebar shows a tree called Servers, and I had two entries under it. I assumed that meant two PostgreSQL server processes.&lt;/p&gt;

&lt;p&gt;It doesn't. In pgAdmin, a "Server" is a saved connection profile — a stored host and port. Two entries means "I know how to reach two PostgreSQL servers," the same way two contacts in your phone don't mean two phones.&lt;/p&gt;

&lt;p&gt;That's why one of mine showed a red X. The server wasn't gone; pgAdmin just wasn't currently connected to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell past me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a containerised app can see its database and your GUI client can't, don't start by suspecting your app. Ask which database each one is actually reaching. Two commands answer it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;netstat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ano&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findstr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;5432&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first tells you what you're connected to. The second tells you who owns the port.&lt;/p&gt;

&lt;p&gt;And the general lesson, which took me longer than it should have: inside Docker and outside Docker are two different networks. Containers reach each other by service name and never touch your host's ports. Everything else — your GUI client, your browser, curl — can only get in through a published port, and that port is competing with every other thing installed on your machine.&lt;/p&gt;

&lt;p&gt;Same number, different world.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>devops</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>The import that worked for months and broke on my first deploy</title>
      <dc:creator>VidhiDixit2000</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:40:00 +0000</pubDate>
      <link>https://dev.to/vidhidixit2000/the-import-that-worked-for-months-and-broke-on-my-first-deploy-5a3l</link>
      <guid>https://dev.to/vidhidixit2000/the-import-that-worked-for-months-and-broke-on-my-first-deploy-5a3l</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not resolve "../styles/Randomfacts.css" from "src/pages/Randomfacts.tsx"
Error: Command "npm run build" exited with 1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was my first deploy to Vercel, and my first thought was that I'd broken something in that commit.&lt;/p&gt;

&lt;p&gt;I opened Randomfacts.tsx and found it importing Randomfacts.css — capital R. Then I checked the actual file sitting in src/styles/, and its name was randomfacts.css, lowercase.&lt;/p&gt;

&lt;p&gt;So the import was wrong. Fine. But that raised a much more confusing question:&lt;/p&gt;

&lt;p&gt;Why had npm run build never thrown this error on my machine?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Windows filesystems are case-insensitive. Ask for Randomfacts.css when the file is called randomfacts.css and Windows hands it over without comment. Both spellings resolve to the same file, so the mismatched import worked locally for months.&lt;/p&gt;

&lt;p&gt;Vercel builds on Linux, which compares filenames byte for byte. There, R and r are genuinely different names — and the one my import asked for didn't exist.&lt;/p&gt;

&lt;p&gt;Same code, same import, two different answers. The operating system answered, not the bundler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I matched the import to the real filename. Then a Windows wrinkle worth knowing: a case-only rename won't be recorded by git, so if you rename the file instead of the import, it takes two steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;mv &lt;/span&gt;src/styles/randomfacts.css src/styles/temp.css
git &lt;span class="nb"&gt;mv &lt;/span&gt;src/styles/temp.css src/styles/Randomfacts.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How to catch these before the build does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use git ls-files rather than ls. It shows the name git actually recorded, which is what the build server checks out — your local filesystem will happily lie to you here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-files src/styles/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that output against your imports. Any casing mismatch is a build failure waiting for your next deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I took from it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a whole class of bugs that only exist when your deploy environment differs from your development one. Case sensitivity is the most common. Path separators and missing environment variables are close behind. None of them are visible while you're only running things locally, and none have anything to do with your logic being wrong.&lt;/p&gt;

&lt;p&gt;I'd been building this project on and off for months — adding functionality in bursts, brushing up on different concepts along the way — and only hit this on the first deploy. It would have taken ten seconds to spot if I'd deployed at the start.&lt;/p&gt;

&lt;p&gt;That's the real lesson for me. Deploy early, even when the project isn't finished. Not for the URL, but because the deploy environment finds a category of bug that your machine structurally cannot.&lt;/p&gt;

</description>
      <category>react</category>
      <category>linux</category>
      <category>git</category>
      <category>vercel</category>
    </item>
  </channel>
</rss>
