Dockerising Your Next.js App, No Fuss!
Share
Building a web application is like creating a masterpiece, but deploying it can feel like a whole different art form. 🎨 You've built a killer Next.js app, and now you're ready to show it to the world. You could use a managed platform like Vercel or Netlify, which are fantastic for quick deployments. But what if you want to be a bit more hands-on? What if you want to give your app its own cosy little home that you control completely? Enter Docker.
Containerisation with Docker has become the go-to for deploying web applications, and for good reason. It wraps your app and all its dependencies into a neat, self-contained package. This means no more "but it worked on my machine!" moments because the environment is consistent everywhere, from your laptop to the production server. It's like giving your app its own dedicated, self-contained environment.
Next.js's "Standalone" Superpower: Slimming Down for Docker!
Before we get into the nitty-gritty of the Dockerfile, there's a little secret
weapon in Next.js that makes this whole process way easier: the
output: 'standalone' option in your next.config.ts. It's a game-changer!
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* other config options here */
output: "standalone",
};
export default nextConfig;
Introduced in Next.js 12, this feature is the Marie Kondo of deployment.
When you enable it, Next.js tidies up your app and creates a tiny
.next/standalone folder containing only the essential files needed to run your
app in production, including a minimal node_modules and a server.js file.
This means you don't have to lug your entire, gargantuan node_modules
directory around. The result? A much lighter, self-contained package that's
basically a perfect fit for a Docker container.
Docker & Next.js: The Multi-Stage Build Tango 💃
Crafting a robust Docker image for a Next.js app is a crucial step towards creating efficient and scalable web applications. We'll be using a multi-stage build approach, which is the gold standard for Docker. Think of it as a fancy cooking show: we'll have separate stations for prepping ingredients, mixing, and finally, plating the dish, but we'll only serve the final, delicious product.
This approach separates the messy build environment from the lean, clean
production environment. The result is a lightweight and secure final image that
won't clutter up your server. We'll break down a typical Dockerfile for a
Next.js app, explaining each stage so you can containerise your own projects
like a pro.
The Build Stages
Our Dockerfile is a symphony of separate acts, each with a specific purpose.
We'll create temporary images to handle specific tasks, then copy only the
essential bits to the final production image. This results in a small, optimised
image.
.
Stage 1: The base Image
FROM node:22-alpine AS base
ARG PORT=3000
This is the foundation, our starting point. We're using a lean, mean
node:22-alpine image, which is based on the famously tiny Alpine Linux. We'll
tag it as base so everyone knows where the party started. The ARG PORT=3000
line sets a default PORT value, like a customisable sticker on your container.
Stage 2: The deps Image (Dependency Installation)
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* ./
RUN yarn --frozen-lockfile
This stage starts from our base image and is dedicated to one thing: getting
our dependencies in order. We install libc6-compat to ensure our Node
environment plays nicely on Alpine, and WORKDIR /app sets our virtual office.
The COPY command is key: it copies only the files needed for dependency
installation. This is a crucial caching trick that saves you time if your
dependencies don't change. Finally, yarn --frozen-lockfile ensures a
reproducible build, which is a fancy way of saying it guarantees the exact same
result every time.
Stage 3: The builder Image (Application Build)
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV ENV=production
RUN yarn build
Our next act, the builder stage, is where the main event happens. We're not
reinstalling dependencies; instead, we're smartly grabbing the node_modules
folder from the deps stage. We then copy in all of our source code
(COPY . .). We set a couple of environment variables (ENV) to politely tell
Next.js to stop phoning home and ensure our app knows it’s time to get serious.
Finally, yarn build kicks off the Next.js compilation.
Stage 4: The runner Image (Production)
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=$PORT
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE $PORT
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
This is the grand finale, the final image we've been working towards! It starts
from the lean base image to ensure our final product is as small and secure as
possible.
We set more environment variables, including NODE_ENV=production for
high-performance mode. The addgroup and adduser commands are a huge security
win: we're creating a new, non-root user (nextjs) to run our app, which
means it won't have more permissions than it needs.
The COPY commands grab our essentials from the builder stage, and the
--chown flag ensures these files are owned by our new, non-root user. The
USER nextjs command locks it down, ensuring everything after this point runs
with limited permissions.
EXPOSE $PORT is a memo for Docker, letting it know which port to listen on,
and ENV HOSTNAME="0.0.0.0" is a quirky detail that ensures Next.js listens on
all network interfaces.
Finally, CMD ["node", "server.js"] is the command that gets your Next.js app
up and running.
To see the complete Dockerfile and more important context, check out this GitHub repository:
nextjs-docker-image
.
Parting Words... for Now! 👋
See? Containerising your Next.js app doesn't have to be scary. By embracing multi-stage builds and Next.js's standalone output, you can create a lightweight, secure, and reproducible deployment. Now you've got the power to take your app anywhere, from your local machine to a cloud server, and know it will behave exactly the same way. So go forth and containerise—your future self will thank you!