← Back to blog

How to Deploy Next.js to a Docker Container: Complete 2025 production guide

September 12, 2025 · Guides

Vercel makes deploying Next.js apps ridiculously easy until you get your first surprise bill. However, one viral post later, and you might be facing a anxiety-inducing invoice. A popular app serving millions of requests can easily cost thousands per month on Vercel, while the same traffic can run comfortably on a $20 VPS with Docker.

Beyond cost savings, Docker gives you complete deployment flexibility. Deploy to any cloud provider, use specific Node.js versions, set up custom caching strategies, or integrate with your company's existing infrastructure. Docker packages your Next.js app with everything needed to run consistently anywhere, eliminating deployment surprises while keeping your hosting costs predictable and under your control.

By the end of this guide, you will have learnt how to deploy nextjs to a Docker container and have it production-ready in no time.

Prerequisites

Before you start, make sure you have:

  • A VPS running Linux (Ubuntu/Debian recommended).
  • Docker and Docker Compose installed.
  • A Next.js app (you can generate one with npx create-next-app@latest).

Setting next.js to standalone mode

Before writing our Dockerfile, your Next.js application needs one critical configuration change. Open your next.config.js file and enable standalone output mode:

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

module.exports = nextConfig

The standalone output tells Next.js to bundle everything needed to run your application into a single directory. This eliminates dependency resolution nightmares and dramatically reduces your container's final size.

Create a Production-Ready Dockerfile

For our tutorial, we'll be following Vercel's recommended multi-stage build dockerfile. Multi-stage build processes ensure we're separating build time dependencies from runtime dependencies which typically reduces image size, resulting in faster deployments and lower storage costs.

# syntax=docker.io/docker/dockerfile:1

FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

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

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

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

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/public ./public

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
ENV HOSTNAME="0.0.0.0"

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

The build process in this Dockerfile is designed for both speed and security. By copying only the package files first, it takes advantage of Docker’s layer caching, meaning future builds are much faster when dependencies haven’t changed. Using the lightweight node:20-alpine base image keeps the build size small, which translates into quicker deployments.

Building and Testing Your Container

Build and run your container to verify everything works:

# Build the Docker image
docker build -t my-nextjs-app .

# Run the container
docker run -p 3000:3000 my-nextjs-app

Open http://localhost:3000 to verify your app loads correctly.

Production Optimizations

Now that our base image is up and running, we can optimize a bit further for a production environment. Firstly, we'll add a .dockerignore file to the project root which will tell Docker not to include any files or folders listed in it when building images, similar to a .gitignore file.

node_modules
.next
.git
.env.local
README.md
Dockerfile

Secondly, we'll add a restart policy either in your docker run command or docker-compose.yml file to always restart the container if it crashes. This is because the Dockerfile runs Next.js as PID 1, which means when the Node.js process exits, the entire container exits too:

docker run -d -p 3000:3000 --restart unless-stopped my-nextjs-app

Common Issues and Solutions

Static assets not loading: Ensure your public folder copies correctly in the Dockerfile. Consider using a CDN for production static assets.

API routes returning 404: Verify your API routes are included in the build. Check that pages/api or app/api directories are copied properly.

Memory issues: Set Node.js memory limits for production containers:

ENV NODE_OPTIONS="--max_old_space_size=1024"

Container exits immediately: Check the container's logs with docker logs my-nextjs-app

Shipping Your Image to GitHub Container Registry

Once your Docker image works locally, you'll want to store it somewhere your production servers can access. GitHub Container Registry (GHCR) is free for public repositories and offers generous storage for private ones.

First, build and tag your image for GHCR:

# Build with GHCR tag  
docker build -t ghcr.io/yourusername/my-nextjs-app:latest .  
  
# Login to GitHub Container Registry  
echo $GITHUB_TOKEN | docker login ghcr.io -u yourusername --password-stdin  
  
# Push the image  
docker push ghcr.io/yourusername/my-nextjs-app:latest

You'll need a GitHub Personal Access Token with write:packages permission. Create one in GitHub Settings > Developer Settings > Personal Access Tokens.

Using Your GHCR Image in Production

With the image created and pushed to GHCR, we can now use in a docker compose file. Our example below shows setting the nextjs app up as a service alongside a PostgreSQL database.

version: '3.8'  
  
services:  
  app:  
    image: ghcr.io/yourusername/my-nextjs-app:latest  
    ports:  
      - "3000:3000"  
    environment:  
      - NODE_ENV=production  
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp  
    depends_on:  
      - db  
    restart: unless-stopped  
  
  db:  
    image: postgres:15-alpine  
    environment:  
      - POSTGRES_DB=myapp  
      - POSTGRES_USER=postgres    
      - POSTGRES_PASSWORD=password  
    volumes:  
      - postgres_data:/var/lib/postgresql/data  
    restart: unless-stopped  
  
volumes:  
  postgres_data:

This kind of setup means you can run your nextjs app alongside any other services you need, such as a database, caching, or other apps completely independent of Vercel's platform and pricing.

Beyond the Basics

This Docker setup gives you complete control over your Next.js deployment. Not only does it run consistently across environments, but it also scales predictably, and costs a fraction of platform-as-a-service alternatives. More importantly, you own your deployment pipeline and can customize it however your project needs.

Managing Docker containers across multiple servers can get complex as you scale. If you're running production workloads and want the benefits of containerization without the operational overhead, Serversinc handles container orchestration, multiple environments, monitoring and more. Get all the cost savings of self-hosted Docker with the simplicity of a managed platform.