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
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
Stop everything:
docker compose down
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"]
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"]
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:
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>
Inspect running containers
docker exec -it vblog-backend sh
ls /app/node_modules/.prisma/client
Configure Prisma for the correct platform
Add this to your Prisma schema:
binaryTargets = ["native", "debian-openssl-3.0.x"]
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)