Containerization for Next.js Developers
7 min read
August 19, 2025
I have been using Next.js for more than a year. The developer experience it offers cannot be matched. Before I used Next.js, I used to work with MERN stack applications extensively. The idea of spinning two servers in order to build and deploy an application bothered me a bit. But, as a beginner, it was so much fun to work with APIs in Node.js. Once I started using Next.js more, I came to realize how easy it became for me to deploy applications without any overwhelming chain of commands. Not only do I like the tech stack I work with, but the hosting provider Vercel is also built with developer experience in mind. The fact that they have solved this problem from a developer’s perspective is admirable.
As I mentioned earlier, I used to deploy all my projects on Vercel, as it requires nothing more than GitHub integration of the project I work on. According to me, I haven’t seen a workflow clearer than this one. Without any additional configuration files like GitHub workflows, I was able to push changes and they would eventually be deployed on Vercel. To start additional services like a database, I don’t have to look for other third-party services anymore since Vercel covers everything for me. For beginners, I don’t think a developer needs more than the managed services provided by Vercel.
Recently, I was working on a static website. I had to use a lot of images for the case study pages. I heavily depended on the Next.js <Image />
component features, like optimization and lazy loading. Since I was on a hobby plan, I pretty much consumed everything it offered. Now, in order to see the images on my website, I have to upgrade my plan to Pro. I know that I could have replaced the Next.js <Image />
components with regular ones. But considering all the prospects, I wanted to move away from Vercel to experiment with what more I could do with the project — and I was not willing to pay that much money to deploy a static website on Vercel. So, I started looking for other options and that’s when I got introduced to containerization.
After playing with my application a lot in the process of containerization, I decided to purchase either a VPS or a managed hosting provider like Railway or Fly.io. Finally, I purchased Railway for its simplicity and database integration. But honestly speaking, I never thought containerizing a Next.js app would be this simple.
To prepare my local codebase before pushing it to production, I decided to run my Dockerfile locally. I will walk you through the project requirements as below. The system should have Docker installed, and after your account integration, that’s it — your system is now ready to run, build, and test your Docker container.
// if you working on windows you might have to execute this command
wsl --update
docker --version
// you can run this to check if everything works fine from your docker interface
docker run hello-world
Once you are done with your development stage, you have to follow the steps below to ensure an error-free process. Finally, the container will be run by Docker.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* add this in your next.config.ts file */
output: "standalone",
};
export default nextConfig;
# syntax=docker.io/docker/dockerfile:1
FROM node:22-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* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable 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 . .
# ARG TABLE_NAME
# ARG RECAPTCHA_SECRET
# ARG NEXT_PUBLIC_RECAPTCHA_SITE_KEY
# ARG NEXT_PUBLIC_PLANNER_ID
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ARG DATABASE_URLENV DATABASE_URL=$DATABASE_URL
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
# Uncomment the following line in case you want to disable 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
# 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 3000
# ARG HOSTNAME
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
Additionally, the Dockerfile includes DATABASE_URL to demonstrate how this project is deployed on the Railway platform. The ARG term is important when deploying on Railway. You can also include a .dockerignore file in the same directory.
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Local env files
.env*.local
.env
# Vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Next.js
.next/
out/
# Production
build
# Misc
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
# IDEs
.vscode/
.idea/
# Testing
coverage/
# Cache directories
.cache/
Once you are configured everything, it is ready to set of and you can execute the following commands to build and run the container.
# Build the Docker image
// Typically, thic command would be enough to build the container.
docker build -t my-nextjs-app .
// But, since we have integrated database, we have to add during the build Mandatorily.
docker build --build-arg DATABASE_URL="postgresql://..." -t my-nextjs-app .
# Run the container
// Same goes to here as well. This would be enough if your app doesn't need any run time variable.
docker run -p 3000:3000 my-nextjs-app
// We should also add the Database url while we run the app as well.
docker run -e DATABASE_URL="postgresql://..." -p 3000:3000 my-nextjs-app
It is important to feed the environment variable during both build time and runtime, as Docker cannot fetch data from the .env
file in your local repository. The process is simple if you are deploying the app on Railway. You just have to add the key and value of the DATABASE_URL
in the environment variables section. Do not forget to add the variable name in the Dockerfile with the ARG
prefix.
If you find this process complex when using Docker locally and think it could be simplified, I have another method to suggest. Create the following file in the same path and execute the command below to build and run the container.
version: "3.9"
services:
app:
build:
context: .
args:
DATABASE_URL: ${DATABASE_URL}
environment:
- DATABASE_URL=${DATABASE_URL}
ports:
- "3000:3000"
docker-compose up --build
This way you can bring your local application live. In terms of the platform, If you’re starting out, stick with Vercel. If you want more control and scalability, try Docker + Railway.