<?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: Abhinav</title>
    <description>The latest articles on DEV Community by Abhinav (@abhinav-balki).</description>
    <link>https://dev.to/abhinav-balki</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%2F3862765%2F9b88a2c3-532f-4dea-8332-150e8b147410.jpg</url>
      <title>DEV Community: Abhinav</title>
      <link>https://dev.to/abhinav-balki</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhinav-balki"/>
    <language>en</language>
    <item>
      <title>I Dockerized a Production AI System as an Intern. Here's What Actually Mattered.</title>
      <dc:creator>Abhinav</dc:creator>
      <pubDate>Mon, 06 Apr 2026 04:49:03 +0000</pubDate>
      <link>https://dev.to/abhinav-balki/i-dockerized-a-production-ai-system-as-an-intern-heres-what-actually-mattered-2bmd</link>
      <guid>https://dev.to/abhinav-balki/i-dockerized-a-production-ai-system-as-an-intern-heres-what-actually-mattered-2bmd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;No CI/CD. No Kubernetes. Just PuTTY, WinSCP, and a system that needed to stop being fragile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The System I Walked Into
&lt;/h2&gt;

&lt;p&gt;I'm an intern on an AI team. My project is an internal AI support tool: an augmented RAG-based system that ingests knowledge bases, searches resolved tickets via vector similarity, and synthesizes resolutions using an LLM. FastAPI backend, React frontend, PostgreSQL with pgvector, ChromaDB for embeddings, OpenAI for generation.&lt;/p&gt;

&lt;p&gt;The AI pipeline is interesting. The infrastructure it was running on was not.&lt;/p&gt;

&lt;p&gt;Here's what "deployment" looked like when I joined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Directory A (test):
  └── git pull → run uvicorn directly on EC2

Directory B (production):
  └── teammate manually copies changed files from Directory A
  └── find-and-replace URLs
  └── run uvicorn directly on EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four ports exposed + separate frontend and backend for both environments. No containerization. No build step for the frontend. No rollback mechanism. No isolation between test and production beyond "they're in different folders." The frontend was not even served via Vite's dev server in production.&lt;/p&gt;

&lt;p&gt;If the EC2 instance had a bad day, reconstruction was from memory and hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fqtj4192i26w3gweit981.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.amazonaws.com%2Fuploads%2Farticles%2Fqtj4192i26w3gweit981.png" alt="Docker-Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One directory. Docker Compose with overlay files for environment separation. nginx as a reverse proxy (two ports instead of four). Image versioning with semantic tags and timestamp backups. A deploy script. A rollback script. Full isolation between prod and test + different Docker networks, different data volumes, different container names.&lt;/p&gt;

&lt;p&gt;Deploy went from "copy files and pray" to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./deploy.sh prod 1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Constraints That Shaped Everything
&lt;/h2&gt;

&lt;p&gt;This is the part I actually want to talk about. The Docker setup isn't novel... anyone can follow a tutorial. What made this interesting was what I couldn't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  No CI/CD
&lt;/h2&gt;

&lt;p&gt;No GitHub Actions. No webhooks. No automated pipelines. My deployment tools are PuTTY (SSH terminal) and WinSCP (file transfer). That's it. So I built a shell script that acts as a poor-man's pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./deploy.sh [stack] [version] [branch]
     │
     ├── git pull origin [branch]
     ├── tag current running images as backup
     ├── docker compose build
     ├── docker compose up -d
     └── prune dangling images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The branch argument means I can test feature branches on the test stack without merging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./deploy.sh &lt;span class="nb"&gt;test &lt;/span&gt;latest feature/new-rag-pipeline
&lt;span class="c"&gt;# verify on internal IP&lt;/span&gt;
&lt;span class="c"&gt;# merge PR&lt;/span&gt;
./deploy.sh prod 1.3 main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this as good as GitHub Actions with automated tests and staging environments? No. Does it work reliably for a single-server deployment with one developer? Yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Secrets, Different Environments
&lt;/h2&gt;

&lt;p&gt;The backend reads its config from a single .env file: database credentials, API keys, OIDC settings. Both prod and test use the same file because they're on the same machine talking to the same database.&lt;/p&gt;

&lt;p&gt;But the OIDC redirect URIs must differ between environments. Prod redirects to the public DNS. Test redirects to the internal IP.&lt;/p&gt;

&lt;p&gt;The solution: Docker Compose's precedence rules. environment: in a compose file beats env_file:. So the base compose file loads the shared secrets via env_file:, and each overlay (prod.yml, test.yml) overrides just the OIDC URI via environment:.&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="c1"&gt;# docker-compose.yml (base)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend/.env&lt;/span&gt;  &lt;span class="c1"&gt;# shared secrets&lt;/span&gt;

&lt;span class="c1"&gt;# docker-compose.prod.yml (overlay)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OIDC_REDIRECT_URI_FRONTEND=https://public.dns.com&lt;/span&gt;

&lt;span class="c1"&gt;# docker-compose.test.yml (overlay)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OIDC_REDIRECT_URI_FRONTEND=http://&amp;lt;IP&amp;gt;:&amp;lt;Port&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a small detail. It's also the kind of thing that causes a two-hour debugging session if you don't know about it. env_file values get silently overridden by environment values, and there's no warning, no log, nothing. You just get the wrong redirect and stare at your OIDC provider's error page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Volume Isolation Without Duplication
&lt;/h2&gt;

&lt;p&gt;ChromaDB stores embeddings on disk. The knowledge base files live on disk. Logs go to disk. Prod and test need completely separate copies of all of these. You don't want a test run corrupting production embeddings.&lt;/p&gt;

&lt;p&gt;Docker Compose variable substitution handles this:&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="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backend/${CHROMA_DIR:-chromaDB}:/app/chromaDB&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backend/${DATA_DIR:-data}:/app/data&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backend/${LOGS_DIR:-logs}:/app/logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.test
&lt;/span&gt;&lt;span class="py"&gt;CHROMA_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;chromaDB-test&lt;/span&gt;
&lt;span class="py"&gt;DATA_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;data-test&lt;/span&gt;
&lt;span class="py"&gt;LOGS_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;logs-test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default values point to prod directories. When you pass --env-file .env.test, the paths switch to test directories. Same compose file, different data. The deploy script handles this automatically, and you never pass --env-file manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Override File Pattern
&lt;/h2&gt;

&lt;p&gt;Docker Compose has a feature where docker-compose.override.yml is automatically loaded alongside docker-compose.yml, but only when you don't use explicit -f flags.&lt;/p&gt;

&lt;p&gt;I used this to create three distinct modes from the same codebase:&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;# Local development (override.yml auto-loaded)&lt;/span&gt;
docker compose watch
→ Vite dev server with HMR, OIDC pointing to localhost

&lt;span class="c"&gt;# Production (explicit -f, override.yml skipped)&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod.yml up
→ nginx serves built static files, OIDC pointing to public DNS

&lt;span class="c"&gt;# Test (explicit -f, override.yml skipped)  &lt;/span&gt;
docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env.test &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.test.yml up
→ nginx serves built static files, OIDC pointing to internal IP, &lt;span class="nb"&gt;test &lt;/span&gt;volumes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One base file. Three overlays. Three completely different behaviors. The developer never thinks about which files to compose: docker compose watch just works locally, and ./deploy.sh picks the right overlay on EC2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;The scariest part was the cutover. Two directories, both running live. I needed to consolidate into one without downtime on prod.&lt;br&gt;
The sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop old test stack (directory A)&lt;/li&gt;
&lt;li&gt;Stop old prod stack (directory B)
&lt;/li&gt;
&lt;li&gt;Copy prod data (ChromaDB, knowledge base, secrets) into directory A&lt;/li&gt;
&lt;li&gt;Create isolated test directories (start empty)&lt;/li&gt;
&lt;li&gt;Pull latest code with all new compose files&lt;/li&gt;
&lt;li&gt;Deploy prod from directory A → verify public DNS works&lt;/li&gt;
&lt;li&gt;Deploy test from directory A → verify internal IP works&lt;/li&gt;
&lt;li&gt;Archive directory B&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 6 is where you sweat. The public DNS now needs to resolve to the new container, with the right OIDC config, serving the right data. If anything is wrong, users see a broken page.&lt;/p&gt;

&lt;p&gt;It worked on the first try. Which means I probably over-prepared, but I'd rather over-prepare than explain to the team why production is down.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rollback
&lt;/h2&gt;

&lt;p&gt;The deploy script tags current images before rebuilding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resolvyst-backend:latest  →  resolvyst-backend:v1.2
                          →  resolvyst-backend:20260403_1430
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback reuses the saved image without touching data volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./rollback.sh prod v1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is basic, but it's infinitely better than what existed before (nothing). The timestamp tag is insurance. Even if you forget to bump the version, you can still roll back to any previous deploy by timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently With More Access
&lt;/h2&gt;

&lt;p&gt;If I had CI/CD and wasn't constrained to PuTTY:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health checks in the compose file. Right now, deploy.sh reports success even if the backend crashes on startup. A curl check post-deploy would catch that.&lt;/li&gt;
&lt;li&gt;Separate secrets per environment. The shared .env file works but is fragile; one wrong edit affects both stacks.&lt;/li&gt;
&lt;li&gt;Automated smoke tests after deploy. Hit the health endpoint, verify the RAG pipeline returns a response, check that OIDC redirects correctly.&lt;/li&gt;
&lt;li&gt;Git working tree check at the top of deploy.sh. Right now, nothing stops you from deploying with uncommitted changes on EC2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But these are improvements to a system that already works. The first version doesn't need to be perfect. It needs to be better than what it replaced, and "someone manually copy-pasting files" is a low bar to clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Takeaway
&lt;/h2&gt;

&lt;p&gt;The interesting skill in infrastructure work isn't knowing Docker or nginx or Compose. It's designing around constraints you can't remove. I couldn't set up CI/CD. I couldn't get a second server. I couldn't change the OIDC provider's configuration beyond adding redirect URIs. I had an intern's access level.&lt;/p&gt;

&lt;p&gt;So I built something that works within those constraints. It's not elegant by industry standards. But it's reproducible, it's rollback-safe, it has environment isolation, and it replaced a process that depended on one person's memory of which files to copy where.&lt;/p&gt;

&lt;p&gt;That's the gap between knowing tools and doing systems design. Tools are things you learn. Systems design is figuring out what to do when the tools you want aren't available.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I'm an intern working on AI systems: RAG pipelines, support ticket analytics, UX upgrades and apparently now DevOps. If you're working on similar problems or just want to talk about building things under real-world constraints, I'd love to connect.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
