How to Deploy a Turborepo with Next.js and NestJs
Hosting on Vercel? This is not your blog. This guide is for those deploying Next.js frontend and NestJs backend from a Turborepo into custom environments like VPS, bare metal, Docker or Kubernetes.
Repo Structure
1apps/2web/ ← Next.js 15.2.1 (Frontend)3api/ ← NestJS 11.1.2 (Backend)4packages/ ← Shared libraries
Prerequisite: Docker Installed
Ensure you have Docker installed on your system where you are going to deploy the Turborepo. If not, refer to Docker official docs
Step 1: Configure Next.js (apps/web)
Next.js needs to be run in standalone mode for optimal Docker performance
Edit apps/web/next.config.ts or .js
1module.exports = {2output: 'standalone',3}
Step 2: Create Dockerfile for Next.js
Create a Dockerfile in Next.js folder, in our case it is apps/web/Dockerfile
1# Stage 1: Install dependencies2FROM oven/bun:1-alpine AS deps3WORKDIR /app4COPY package.json bun.lock turbo.json ./5COPY apps/web/package.json ./apps/web/6COPY packages/ ./packages/7RUN bun install89# Stage 2: Build the Next.js application10FROM oven/bun:1-alpine AS builder11WORKDIR /app12ENV NODE_OPTIONS="--max-old-space-size=4096"13COPY . .14RUN bun install15RUN bun run build --filter=web --concurrency=11617# Stage 3: Prepare the runtime18FROM oven/bun:1-alpine AS runner19WORKDIR /app20RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs21COPY --from=builder /app/apps/web/.next/standalone ./22COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static23COPY --from=builder /app/apps/web/public ./apps/web/public24RUN chown -R nextjs:nodejs /app25USER nextjs26EXPOSE 300027CMD ["node", "apps/web/server.js"]
Tested and working - feel free to copy paste, but always strive to understand the concept.
Vijayaraghavan
Founder - Asynx Devs
Step 3: Dockerfile for NestJS (apps/api)
Let's create a Dockerfile for our NestJS backend. In our case it's apps/api/Dockerfile
1FROM oven/bun:1-alpine AS deps2WORKDIR /app3COPY package.json bun.lock turbo.json ./4COPY apps/api/package.json ./apps/api/5COPY packages/ ./packages/6RUN bun install78FROM oven/bun:1-alpine AS builder9WORKDIR /app10COPY . .11RUN bun install12RUN bun run build --filter=api1314FROM oven/bun:1-alpine AS runner15WORKDIR /app16RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nestjs17COPY --from=deps /app/node_modules ./node_modules18COPY --from=builder /app/apps/api/dist ./dist19COPY --from=builder /app/apps/api/package.json ./package.json20RUN chown -R nestjs:nodejs /app21USER nestjs22EXPOSE 300123ENV NODE_ENV=production PORT=300124CMD ["bun", "run", "start:prod"]
Tested and working - feel free to copy paste, but always strive to understand the concept.
Step 4: Create a Docker compose yaml
At the root of your folder (not at apps/, it must be located where apps, turbo.json is located), create a docker-compose.yml:
1version: '3.8'23services:4web:5build:6context: .7dockerfile: apps/web/Dockerfile8ports:9- "3000:3000"10environment:11- NODE_ENV=production12env_file:13- apps/web/.env.production14depends_on:15- api16networks:17- app-network1819api:20build:21context: .22dockerfile: apps/api/Dockerfile23ports:24- "3001:3001"25environment:26- NODE_ENV=production27- PORT=300128networks:29- app-network3031networks:32app-network:33driver: bridge
Step 5: Build & Run
docker compose -f docker-compose.yml up -d --build
That's it's done, pull the image and run the container on any system of your preference.
Here are the common pitfall's and misconfigurations while deploying a Turborepo:
1. Turbo.json Misconfiguration:
Here's a Tested & Working turbo.json config for this setup:
1{2"$schema": "https://turborepo.com/schema.json",3"ui": "tui",4"tasks": {5"build": {6"dependsOn": ["^build"],7"inputs": ["$TURBO_DEFAULT$", ".env*"],8"outputs": [".next/**", "!.next/cache/**", "dist/**", "build/**"]9},10"web#build": {11"dependsOn": ["^build"],12"inputs": ["$TURBO_DEFAULT$", ".env*"],13"outputs": [".next/**", "!.next/cache/**"]14},15"api#build": {16"dependsOn": ["^build"],17"inputs": ["$TURBO_DEFAULT$", ".env*"],18"outputs": ["dist/**"]19},20"lint": {21"dependsOn": ["^lint"]22},23"check-types": {24"dependsOn": ["^check-types"]25},26"dev": {27"cache": false,28"persistent": true29}30}31}
2. Frontend ↔ Backend Communication:
When communicating between frontend and backend inside Docker never use localhost.
✅ Use: http://api:3001
❌ Don’t use: http://localhost:3001
This works because api is the Docker service name, and both containers are on the same bridge network. Check your Dockerfile created in the above steps to determine the name usage. In our case it is api.
Deployment Tip
Once your image is build, you can export it using:
docker save -o myapp.tar <image_name>
Then transfer to another server and run:
docker load -i myapp.tar
docker compose up -d
☸️ Kubernetes Deployment?
🧭 If you're looking to host this into a Kubernetes (k8s/k3s). check out our guide: How to Deploy a Turborepo into Kubernetes.
Conclusion
You now have a production ready, scalable deployment setup for your Turborepo app with Next.js and NestJS - fully containerized and platform agnostic. Whether you are deploying it to a VPS, cloud VM or in a Kubernetes Cluster, the foundation is solid.