Create Production Dockerfile for NextJS - Deploy Everywhere

Dockerfiles aren't hard to do. You do it once per framerwork like NextJS and use everywhere!

Docker NextJS deployment
September 29, 2024

Deployment is something most Junior developers are scared of. This is one of the reasons services like Vercel or Heroku are popular and profitable. 

You need to be able to detach yourself from providers that make your profit go away the more you scale. It's smart to Dockerize your application and be able to deploy it everywhere.

In this article we're going to:

  • Explain the benefits of using Docker images
  • Create a NextJS Dockerfile with public environment variables

You can check the whole nextjs-starter we have or check at the bottom for the branch link with the code from this article. The Dockerfile itself is based on the best practices from NextJS documentation with some additions.


Why use Docker

I'll start with a question. How often have you heard - "It works on my machine"? Probably a lot. This is one reason that Docker is good for you. If it works on your machine, it'll work on mine too.

In this case, with NextJS it's not that important for local development as we don't have multiple services architecture, yet for deployment, you will need to have a Docker image.

An example that might interest you is Vercel's pricing. They have enough of a free plan for small MVPs or hobby projects at the moment of writing. The issue is when you go from Free to Pro, you start paying 20$ per member. The bigger the team, the larger the bill. Scaling there is really expensive with resources too.

Don't get me wrong, they have a good service and it's easy to use. It's just not cheap.

Now if you have a Dockerfile prepared, you can just take it and deploy it on AWS, GCP, Fly.io, and even on your own VPS.

I hope that with this you understand why it's good to at least prepare a Dockerfile for you, even if you don't need it now.


Create a NextJS Dockerfile from scratch

The first step is to create a file named Dockerfile at the top level of your project. 

Start with adding the base lightweight image for our build.

FROM node:20.9.0-alpine AS base

This will serve as a base for all our steps along the file. We have a couple of steps in the file:

  • base - Base alpine image as shown above.
  • deps - Our image dependencies are installed with the CI command, you can use yarn, and pnpm too, just change appropriate lines with it.
  • builder - Copying our installed node_modules and building the application.
  • runner - Setting correct NODE_ENV and copying the necessary files from the previous step to run the app.

Here is the Dockerfile in a working state.

FROM node:20.9.0-alpine AS base

FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# 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 3051

ENV PORT=3051

CMD ["node", "server.js"]

You can test it out with the following command in the terminal.

docker build -t nextjs-starter-example .

It should show output similar to the photo. There are possible issues you might face.

  • App router issue with next-intl and dynamic components
  • Incorrect file with next config. Check for the extension if it's mjs or js in your case

Built docker image NextJS

We've now built our NextJS Docker image! 


If you like my articles a subscribe to my newsletter is highly appreciated! It's in the footer below along with my social media accounts!


Dockerfile variables with NEXT_PUBLIC

I'll keep it short. I've done it by allowing myself to create these variables in GCP Cloudbuild Triggers and making them available at build time. It's possible to do it in AWS or any other cloud provider, as it depends on where you're deploying. The important part is that you need to have those at build time!

GCP Build triggers

In the best-case scenario, you can have feature flags and avoid most environments, but for those in another article. You need to add the ARG in the Dockerfile along with the ENV substation. Here is a basic example.

ARG PORT
ENV PORT=${PORT}

Now the full Dockerfile which you probably want already.

ARG PORT
ARG NEXT_PUBLIC_API_URL

FROM node:20.9.0-alpine AS base

FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder

ARG PORT
ARG NEXT_PUBLIC_API_URL

ENV PORT=${PORT}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# 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 3051

ENV PORT=3051

CMD ["node", "server.js"]

You can now Dockerize your NextJS application and deploy it anywhere. It's not complex, but it's good to understand what you're copy/pasting to avoid issues when going solo in your deployment process. Check the branch in the starter if interested.

If you have any questions, hit me up on Twitter/X or LinkedIn. At both places a follow is highly appreciated!

You can subscribe to my newsletter below to get notified about any new articles that are coming out. I'll not spam you!

 

Related categories:DeployNextJS
Share this post:

Related articles

My Neswletter

Subscribe to my newsletter and get the latest articles and updates in your inbox!