DEV Community

Gonçalo Alves
Gonçalo Alves

Posted on

How to serve a Next.js app in docker

Intro

I have created a few Next.js containers, but for some reason, when I do it I always go back in forth between my code and few webpages. I haven't found a page with everything that I need to know in order to containerize an app.

So I'm writing this for my reference and hope that you will also get value from it. If you do, please leave me a comment as way to encourage me to do this more often.

Setting up the next.js project

Ideally you would use a current Next.js project that you already created.

If you currently you don't have a Next.js project and want to tryout this tutorial you can run npx create-next-app@latest to create a new Next.js project and follow the instruction provided in the terminal by the installer.

Next make sure to edit the next.config.js file in order to create the "production" build. Your next.config.js file should look something like this:

/** @type {import('next').NextConfig} */
const nextConfig = { output: 'standalone', }

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

Creating the Dockerfile for x86 and ARM

In order to containerize your app, you must create a file called Dockerfile in the root directory of your project. You should copy the code below to your Dockerfile, depending on the Docker host architecture.

There a few important points to note in the Dockerfile :
1. The first line of the Dockerfile referes to the base image of the container and, in this case, the node version in which your containerized app is going to run. You should always match the version of node you are running in your dev environment with the version running in the container.
2. Some Next.js have different file structures. For instance, some apps need environment variables to be set in order to run properly. In that case you need to copy your .env file to the container. You can do that by adding the following code: COPY --from=builder --chown=nextjs:nodejs /app/.env ./ . This code should be added before the USER nextjs line to ensure that it's copied at the correct moment.
3. You should adapt the code from the previous line to suit your needs. For instance if you have an ORM with a sqlite database, you are going to have to build the database from scratch when you build the container. The same steps that you followed to build the database in your DEV environment, should be followed for the containerized environment. I have left an example in a few comments of how you can achieve that. In my example I'm using sqlite with prisma.

For x86 architecture

FROM node:20-alpine AS base

# Install dependencies only when needed
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

# 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 yarn global add 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 . .

# Example: Create a new database
# RUN rm -rf /app/prisma
# RUN npx prisma init --datasource-provider sqlite
# COPY prisma/schema.prisma ./prisma/
# COPY prisma/migrations ./prisma/
# RUN npx prisma migrate dev --name init
# RUN npx prisma generate

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Comment the following line in case you want to enable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1

# RUN yarn build

# If using npm comment out above and use below instead
RUN npm run build

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

ENV NODE_ENV production
# Comment the following line in case you want to enable 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
# Environment Variable Example: COPY --from=builder --chown=nextjs:nodejs /app/.env ./
# App with Database Example: COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma/
COPY --from=builder --chown=nextjs:nodejs /app/.next/ ./.next/
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
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"]
Enter fullscreen mode Exit fullscreen mode

For the ARM architecture

FROM node:20-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk update && apk add --no-cache libc6-compat python3 make build-base 
WORKDIR /app
# ENV PYTHON /usr/bin/python3
# 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 yarn global add 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 . .

# Example: Create a new database
# RUN rm -rf /app/prisma
# RUN npx prisma init --datasource-provider sqlite
# COPY prisma/schema.prisma ./prisma/
# COPY prisma/migrations ./prisma/
# RUN npx prisma migrate dev --name init
# RUN npx prisma generate

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Comment the following line in case you want to enable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1

# RUN yarn build

# If using npm comment out above and use below instead
RUN npm run build

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

ENV NODE_ENV production
# Comment the following line in case you want to enable 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
# Environment Variable Example: COPY --from=builder --chown=nextjs:nodejs /app/.env ./
# App with Database Example: COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma/
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"]
Enter fullscreen mode Exit fullscreen mode

Build the Docker Container

To build the Docker Container all you have to do is go into the Next.js root directory and issue the following command: `sudo docker build --no-cache -t <IMAGE_NAME>

Be sure to replace <IMAGE_NAME> with a name that you can remember. That name is going to be used in the next step.

Also note that the command assumes that you have a file called Dockerfilein the root of your project. If, for some reason, you want to use a file with a different name you can use the -f <FILENAME> flag (e.g.: sudo docker build --no-cache -t <IMAGE_NAME> -f <FILENAME>

Running the Docker Container

After the container is built, you can deploy the container. For that we are going to use the following command: sudo docker run -p 5000:3000 --name <CONTAINER_NAME> <IMAGE_NAME>

-p 5000:3000 - Maps the port 5000 of the host to the port 3000 of the container which is the default Next.js port.
<CONTAINER_NAME> - The name of the container. This is useful for easy reference of the container when issuing commands or when listing containers (e.g.: docker stop <CONTAINER_NAME>; docker ps)
` - The name of the image (given in the previous step). This is useful for easy reference of the image.

Next steps

One of the ideas behind docker is to maximize efficiency. In this regard, you could use a Caddy instance in another container and use it as reverse proxy for your docker apps. For instance, you could have 3 different domains, point to the same machine in DNS and configure Caddy to serve the apps on the different ports to the different URLS.

If you would me to write about this leave me a comment :).

Connect with me

If you like this article be sure to leave a comment. That will make my day!

If you want to read other stuff by me you can check out my personal blog.

If you want to connect with me you can send me a message on Twitter/X.

You can also check other stuff that I have going on here

Top comments (0)