DEV Community

Cover image for 🐳 How Docker Saved My Full-Stack Project (And My Sanity)
ujjwal
ujjwal

Posted on

🐳 How Docker Saved My Full-Stack Project (And My Sanity)

When I built VBlog, a full-stack application with Next.js, Express, Prisma, and PostgreSQL, everything ran flawlessly on my local machine.

Then I tried deploying it to another environment.

💥 Chaos ensued. Missing dependencies. Mismatched Node versions. Prisma binaries that refused to cooperate. PostgreSQL connection failures. The backend would work while the frontend crashed, or vice versa.

That's when Docker shifted from a "someday" learning goal to an immediate necessity.

In this article, I'll walk you through:

  • Why Docker became essential for my project
  • How I containerized each component of my stack
  • The Prisma binary issue that almost broke me (and how I fixed it)
  • My final docker-compose workflow that ties everything together

🤯 The "Works on My Machine" Nightmare

My tech stack consisted of:

  • Next.js 15 for the frontend
  • Node.js + Express for the backend API
  • Prisma ORM for database operations
  • PostgreSQL as the database
  • JWT for authentication

Everything worked perfectly on my Fedora laptop. But when I tried containerizing the app using Alpine Linux, Prisma immediately complained:

Error relocating query-engine-linux-musl: RELRO protection failed
Enter fullscreen mode Exit fullscreen mode

The frontend built successfully locally but failed inside containers. PostgreSQL would start, but the backend couldn't establish reliable connections during startup.

I was juggling three different environments—my host machine, the backend container, and the frontend container—each with its own quirks and failures.

This is exactly where Docker proves its worth.


🎯 Why Docker Actually Matters

Here's what Docker solved for me:

Consistent environments across machines

  • Identical Node versions
  • Same OS layer
  • Matching Prisma engine binaries
  • Predictable network configuration

Once your app runs in Docker, it runs the same way everywhere Docker is installed—whether that's Linux, macOS, Windows, or cloud infrastructure.

Zero manual dependency installation

No one needs to install Node, PostgreSQL, Prisma, or package managers. Everything runs inside isolated containers.

True component isolation

Each container has a single responsibility. The frontend container runs Next.js. The backend handles Node and Prisma. The database runs PostgreSQL. No version conflicts. No dependency interference.

Infrastructure as code

One file (docker-compose.yml) defines your entire stack.

Start everything:

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

Stop everything:

docker compose down
Enter fullscreen mode Exit fullscreen mode

This approach eliminates roughly 80% of environment setup problems.


🏗️ Backend Dockerfile: Why I Switched from Alpine to Debian

Initially, I chose Alpine Linux for its small image size. But Prisma and Alpine's musl libc implementation don't play nicely together, resulting in constant crashes.

The solution: switch to debian-bullseye, which uses glibc.

FROM node:20-bullseye

WORKDIR /app

COPY package*.json ./

RUN apt-get update && apt-get install -y openssl
RUN npm install

COPY . .

RUN npm run build

EXPOSE 8000

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

After this change:

  • No more binary incompatibility errors
  • Prisma worked reliably on every request
  • The RELRO errors disappeared completely

⚛️ Frontend Dockerfile: Production-Ready Next.js

I used a multi-stage build to keep the final image lean:

FROM node:20-bullseye AS builder
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

FROM node:20-bullseye AS runner
WORKDIR /app

COPY --from=builder /app ./
EXPOSE 3000

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

This produces a production-optimized Next.js build ready for deployment.


🎼 The Complete Docker Compose Setup

Here's how I orchestrated all three services:

version: "3.9"
services:
  postgres:
    image: postgres:16
    container_name: vblog-postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: vblog
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  backend:
    build: ./backend
    container_name: vblog-backend
    security_opt:
      - seccomp:unconfined
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://postgres:password@postgres:5432/vblog?schema=public
      PORT: 8000
    ports:
      - "8000:8000"
    command: >
      sh -c "npx prisma generate &&
             npx prisma migrate deploy &&
             npm start"

  frontend:
    build: ./frontend
    container_name: vblog-frontend
    depends_on:
      - backend
    environment:
      NEXT_PUBLIC_API_URL: http://vblog-backend:8000
    ports:
      - "3000:3000"

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

With this setup, I can launch the entire application stack with a single command, and it behaves identically on any machine.


🔍 Debugging Tips That Saved Hours

View container logs

docker logs <container-name>
Enter fullscreen mode Exit fullscreen mode

Inspect running containers

docker exec -it vblog-backend sh
ls /app/node_modules/.prisma/client
Enter fullscreen mode Exit fullscreen mode

Configure Prisma for the correct platform

Add this to your Prisma schema:

binaryTargets = ["native", "debian-openssl-3.0.x"]
Enter fullscreen mode Exit fullscreen mode

Avoid Alpine unless necessary

The combination of musl libc and Prisma often leads to compatibility headaches.


✨ What I Gained from Dockerizing

The transformation was remarkable:

  • Anyone can clone the repository and start the app with one command
  • No more "Prisma engine not found" errors
  • PostgreSQL connection issues vanished
  • Development and production environments are identical
  • The entire setup is deployment-ready

Docker didn't just fix my environment issues—it made my project portable, predictable, and production-grade.


💭 Final Thoughts

If you're building a full-stack application with Node.js, Next.js, Prisma, and PostgreSQL, Docker should be part of your stack from the beginning.

The debugging time it saves is substantial. The portability is unmatched. And deployment becomes dramatically simpler.

Have you faced similar environment issues in your projects? How did you solve them? I'd love to hear your experiences in the comments below.


Want to see the complete VBlog source code? Check out my GitHub repository.

Top comments (0)