Next.js with Docker Compose for Development

Integrating Docker Compose with Next.js for easy and secure local development, compatible with npm, yarn, and pnpm

Next.js with Docker Compose for Development

This post is part of my Next.js and Docker series — feel free to check out my other posts in these series, or read the previous post:

Multi-stage Production Dockerfile for Next.js
Dockerfile for 5x smaller images, compatible with npm, yarn, and pnpm

In the previous guide, we explored how to create an optimized Dockerfile for your Next.js production deployment, showing how we could shave down the image to 1/5th of its original size.

Today, we’ll create a new Dockerfile, purely for development purposes, and find out how to launch it using Docker Compose for our local development environment!

Why Docker Compose?

First off, why even bother with Docker Compose? Why not just run something like npm run dev and call it a day?

Well, for 2 reasons.

  1. If you’re deploying to production using Docker, you want your development environment to mimic the production one as closely as possible, to avoid any operating system specific bugs.
  2. Managing multi-container Docker applications is a lot easier with Docker Compose. For example, if you have a Next.js app, an API, a database, and Redis all under one hood makes it a lot easier (and safer) to integrate those systems locally.

Prerequisites

  • A Next.js app. Follow this guide if you don’t already have one!
  • Docker engine installed on your system. I personally use OrbStack for this.

Previously… ⏮️

Our optimized production Dockerfile for Next.js looks a little like this:

# Dockerfile.prod 
 
FROM node:18-alpine AS base 

FROM base AS deps 
RUN apk add --no-cache libc6-compat 
WORKDIR /app 

# Install dependencies based on the preferred package manager 
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 
RUN \ 
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 
  elif [ -f package-lock.json ]; then npm ci; \ 
  elif [ -f pnpm-lock.yaml ]; then npm i -g pnpm && pnpm i --frozen-lockfile; \ 
  else echo "Lockfile not found." && exit 1; \ 
  fi 
 
# Rebuild the source code only when needed 
FROM base AS builder 
WORKDIR /app 
COPY --from=deps /app/node_modules ./node_modules 
COPY . . 
 
RUN \ 
  if [ -f yarn.lock ]; then yarn build; \ 
  elif [ -f package-lock.json ]; then npm run build; \ 
  elif [ -f pnpm-lock.yaml ]; then npm i -g pnpm && pnpm build; \ 
  else echo "Lockfile not found." && exit 1; \ 
  fi 
 
 
# Production image, copy all the files and run next 
FROM base AS runner 
WORKDIR /app 
 
ENV NODE_ENV production 
# Uncomment the following line in case you want to disable telemetry during runtime. 
# ENV NEXT_TELEMETRY_DISABLED 1 
 
RUN addgroup --system --gid 1001 nodejs 
RUN adduser --system --uid 1001 nextjs 
 
COPY --from=builder /app/public ./public 
 
# Set the correct permission for prerender cache 
RUN mkdir .next 
RUN chown nextjs:nodejs .next 
 
# Automatically leverage output traces to reduce image size 
# https://nextjs.org/docs/advanced-features/output-file-tracing 
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 
 
USER nextjs 
 
EXPOSE 3000 
 
ENV PORT 3000 
# set hostname to localhost 
ENV HOSTNAME "0.0.0.0" 
 
CMD ["node", "server.js"]

As we previously talked about, this uses a multi-stage build approach to only copy over the necessary files at every step. This takes a tiny bit longer to build, but results in a much smaller image, making it ideal for deploying to production!

Great, but what about local development? 👨‍💻

Well, we don’t need all these bells and whistles, that’s for sure. Something like this will do just fine:

# Dockerfile.dev 
 
FROM node:18-alpine 
 
RUN apk add --no-cache libc6-compat 
WORKDIR /app 
 
# Install dependencies based on the preferred package manager 
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 
RUN \ 
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 
  elif [ -f package-lock.json ]; then npm ci; \ 
  elif [ -f pnpm-lock.yaml ]; then npm i -g pnpm && pnpm i --frozen-lockfile; \ 
  else echo "Lockfile not found." && exit 1; \ 
  fi 
 
COPY . . 
 
CMD \ 
  if [ -f yarn.lock ]; then yarn dev; \ 
  elif [ -f package-lock.json ]; then npm run dev; \ 
  elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ 
  fi

Let’s call this file Dockerfile.dev and put it in our project root. Similar to our production Dockerfile, this will work regardless of which package manager you use (npm, yarn, or pnpm).

The difference here is that after installing dependencies, instead of building the app and copying over files, we’ll simply use one stage, and just run the dev command from our npm scripts.

Finally, let’s also add our Docker Compose file:

# docker-compose.yaml 
 
version: "3.8" 
 
services: 
  nextjs-app: 
    container_name: nextjs-app 
    restart: unless-stopped 
    build: 
      context: . 
      dockerfile: Dockerfile.dev 
    volumes: 
      - .:/app 
      - /app/.next 
      - /app/node_modules 
    ports: 
      - 3000:3000 
    env_file: 
      - ./.env

Here, we’re doing a few things:

  • We create a new container called nextjs-app.
  • We tell Docker Compose to automatically restart our container (e.g. during a crash) unless explicitly stopped.
  • We specify which Dockerfile we’ll be using for building and what the context (project root) of the container is.
  • We mount the current directory as /app in the container, but we exclude our .next and node_modules folders from being mounted. This is because we want to Docker to be fully independent from our system and not use any pre-compiled files or our own operating system’s dependencies.
  • We tell Docker to map the port 3000 on the host to port 3000 in the container, thus exposing our application to http://localhost:3000.
  • We tell Docker to grab our .env file and make all environment variables we have set up there available to our container.

Fire it up! 🔥

Open your terminal and navigate to your project root. Run this command:

docker compose --env-file .env up --build

And voilà, your Next.js app is running in a Docker container! Open your web browser and go to http://localhost:3000 to see it in action. 🚢

Wrapping Up

So there you have it! You’ve successfully added Docker Compose to your Next.js app 🎉 In the next few guides in this series, we'll deploy our dockerized Next.js application to a DigitalOcean Droplet, as well as see what it's like deploying it straight to DigitalOcean App Platform.

Until next time — happy coding!