Tools: Ultimate Guide: I Dockerized a Production AI System as an Intern. Here's What Actually Mattered.
The System I Walked Into
What I Built
The Constraints That Shaped Everything
No CI/CD
Shared Secrets, Different Environments
Volume Isolation Without Duplication
The Override File Pattern
The Migration
Rollback
What I'd Do Differently With More Access
The Actual Takeaway No CI/CD. No Kubernetes. Just PuTTY, WinSCP, and a system that needed to stop being fragile. 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. The AI pipeline is interesting. The infrastructure it was running on was not. Here's what "deployment" looked like when I joined: 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. If the EC2 instance had a bad day, reconstruction was from memory and hope. 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. Deploy went from "copy files and pray" to: 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. 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: The branch argument means I can test feature branches on the test stack without merging: 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. 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. But the OIDC redirect URIs must differ between environments. Prod redirects to the public DNS. Test redirects to the internal IP. 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:. 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. 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. Docker Compose variable substitution handles this: 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. 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. I used this to create three distinct modes from the same codebase: 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. The scariest part was the cutover. Two directories, both running live. I needed to consolidate into one without downtime on prod.
The sequence: 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. 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. The deploy script tags current images before rebuilding: Rollback reuses the saved image without touching data volumes: 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. If I had CI/CD and wasn't constrained to PuTTY: 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. 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. 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. 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. 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse