Markus Oberlehner

Running Next.js with Docker


Nowadays, there are plenty of ways to run and deploy our Next.js application to the World Wide Web. Yet, especially in an enterprise environment or when we don’t want to depend on a particular provider like Vercel, we may consider bundling our Next.js-powered application within a Docker container. Docker containers offer numerous advantages, like portability and a standardized environment. The Docker container we create in the following few steps can be run and deployed on every system capable of running Docker.

The Dockerfile

We use a multi-stage build setup to build our Docker image. This has two main benefits. For once, it helps us to break up our Dockerfile into logical steps, and second, most importantly, it allows for creating a tiny final image by disposing of all the dependencies needed and artifacts created in the steps before the final run stage.

First, we create a new Dockerfile at the root level of our Next.js project and create a new base stage:

# syntax = docker/dockerfile:1

FROM node:22-slim AS base

ARG PORT=3000

ENV NEXT_TELEMETRY_DISABLED=1

WORKDIR /app

We’ll use this base stage as a starting point for all further stages, so this is the perfect place to configure all the general settings we need for all the other stages.

Now, let’s continue with installing all the necessary dependencies in the next stage of our Dockerfile:

# ...

FROM base AS dependencies

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

We use our base stage as a starting point for this stage. By copying only package.json and package-lock.json before running npm ci, we ensure that Docker can efficiently cache the build process and only run npm ci of one of those two files, but not any other file has changed.

# ...

FROM base AS build

COPY --from=dependencies /app/node_modules ./node_modules
COPY . .

# Public build-time environment variables
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

RUN npm run build

For the build stage, we first COPY the dependencies we’ve installed previously and then also COPY our source code from the project root directory into the container. If we need any public environment variables at build time, we must specify them here as ARG’s and later pass them to the docker build command.

Important: We must never expose private data to the client build! We’ll later pass private environment variables to the docker run command.

The last step in this stage is to run npm run build. After that, we’re ready to run our Next.js application:

# ...

FROM base AS run

ENV NODE_ENV=production
ENV PORT=$PORT

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=build /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE $PORT

ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

First, we set the NODE_ENV to production because this is our target environment for this Docker image. After setting the PORT to whatever we pass in as PORT ARG, we create a new user and group, which we’ll use to run the Next.js Node.js process. This ensures that the Node.js process can only access the bar minimum data inside the container, which means a potential attacker can not do too much harm.

After creating a new .next directory and setting its permissions, we COPY only the necessary files from the build stage. Note that we COPY a /app/.next/standalone directory. To make the Next.js build script generate this Docker-optimized standalone build, we need to update our next.config.mjs file:

// next.config.mjs
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

Finally, we can change to the newly created nextjs user, expose the PORT we’ve configured earlier, and start the Node.js server.

Here is the complete Dockerfile:

# syntax = docker/dockerfile:1

FROM node:22-slim AS base

ARG PORT=3000

ENV NEXT_TELEMETRY_DISABLED=1

WORKDIR /app

# Dependencies
FROM base AS dependencies

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

# Build
FROM base AS build

COPY --from=dependencies /app/node_modules ./node_modules
COPY . .

# Public build-time environment variables
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

RUN npm run build

# Run
FROM base AS run

ENV NODE_ENV=production
ENV PORT=$PORT

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=build /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE $PORT

ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

Exclude Files With .dockerignore

To ensure that we don’t copy any unnecessary files into our Docker container, which can lead to longer build times, fewer cache hits, and potentially even security risks, we create a new .dockerignore file and exclude everything that’s not needed to build and run our application:

Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.env*
.next
.git
# Add more files and directories according to your porject needs

With that, we’re ready to build and run our Next.js Docker container.

Building and Running the Next.js Docker Image

To build the Docker image, navigate to your project’s root directory in the terminal and run the following command:

docker build -t my-app .

This command tells Docker to build an image based on the Dockerfile in the current directory . and tag it with the name my-app.

If you have any build-time environment variables, you can pass them using the --build-arg flag:

docker build -t my-app --build-arg NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_key_here .

Running Next.js Using Docker Compose

To streamline the process of running our Next.js Docker image locally, we can use docker compose. docker compose enables us to specify all the parameters and settings necessary to build and run our Docker container in a single docker-compose.yml file.

version: "3"
services:
  my-app:
    build:
      context: .
      args:
        - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
    ports:
      - "3000:3000"
    environment:
      - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}

This configuration tells Docker to build the image using the Dockerfile in the current directory, map port 3000 from the container to port 3000 on the host machine, and set some environment variables.

docker compose up

As you can see, now we only need this simple command to run our Next.js application with Docker instead of having to remember all the parameters for our build and run commands. Furthermore, if at some point we need additional services to run our application, like a PostgreSQL DB, we can configure this in our docker-compose.yml file.

Wrapping It Up

Running Next.js within a Docker container allows us to deploy it to any cloud hosting provider quickly. Not only that, Docker also enables us to eliminate the “works on my machine” phenomenon by allowing us to run our app within a controlled environment on our development laptops.