Standalone Next.js Application in Docker

I have seen a few questions on Stack Overflow relating to building a simple standalone Next.js app in a Docker image. Here’s one way to do it.

Project Layout

This is the project layout:

├── apps
│   └── landing
│       ├── Dockerfile
│       ├── next.config.js
│       ├── package.json
│       ├── pages
│       │   └── index.js
│       └── public
├── docker-compose.yml
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

There is no server.js because this will be built on the image.

The apps/landing/next.config.js configuration specifies that we’re building a standalone application.

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

module.exports = nextConfig

The apps/landing/package.json gives the requirements for the application.

{
  "name": "landing",
  "version": "1.0.0",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "latest",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Docker Image

The following multi-stage Dockerfile will build and then run the application.

FROM node:18.17.0-alpine as base

ENV PNPM_HOME="/var/lib/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

RUN corepack enable

RUN pnpm add -g turbo

# BUILDER ---------------------------------------------------------------------

FROM base AS builder

WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json .
COPY apps/landing ./apps/landing

RUN turbo prune landing --docker
RUN pnpm install --frozen-lockfile

WORKDIR /app/apps/landing

RUN pnpm turbo run build

# RUNNER ----------------------------------------------------------------------

FROM base AS runner

WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

USER nextjs

COPY --from=builder /app/apps/landing/.next/standalone /app

CMD node ./apps/landing/server.js

There are three phases in the Dockerfile: setup, build and run.

Setup

Setup happens directly on the base image.

  1. Set the PNPM_HOME environment variable and add it to the execution path. The PNPM_HOME setting determines where pnpm will install packages.
  2. Enable Corepack.
  3. Use pnpm to install the Turborepo (the turbo package).

Turborepo is a high-performance build system for JavaScript and TypeScript.

Performant NPM (or pnpm) is an alternative package manager for Node.js that’s fast and efficient, reducing package storage requirements.

Builder

The builder stage copies files across from the host onto the image. It then uses turbo to prune the target application, which will trim the project down to contain a minimal set of dependencies. The --docker flag indicates that the project should be optimised for Docker. It then uses pnpm to install the required packages. Finally turbo is run via pnpm and executes the build command to compile and package the project.

The root package.json specifies the build command.

{
  "name": "my-next-app",
  "private": true,
  "workspaces": [
    "apps/landing"
  ],
  "scripts": {
    "dev": "next dev apps/landing",
    "build": "next build apps/landing",
    "start": "next start apps/landing"
  },
  "engines": {
    "npm": "^9.0.0"
  }
}

Runner

The runner stage adds a non-root user, nextjs, and then changes the active user to nextjs. It copies the files for the standalone application across from the builder stage and then uses node to run the application server.

Docker Compose Stack

You could build and run the Docker image. However, to ease the process we’ll create a docker-compose.yml file.

version: '3.8'
services:
  landing:
    image: landing
    container_name: landing
    build:
      context: .
      dockerfile: ./apps/landing/Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production

To build and run:

docker-compose build && docker-compose up

Visit http://127.0.0.1:3000/ to see the running application.

A screenshot of the minimal Next.js application in browser.