Would you like to buy me a ☕️ instead?
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.
node:22-slim
is a minimal image, including only Node and npm.- The default
PORT
ARG
can be overwritten when running the container. NEXT_TELEMETRY_DISABLED=1
prevents Next.js from sending telemetry data to Vercel servers.- We configure the
WORKDIR
within the container to contain our application files.
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.