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.
- Set the
PNPM_HOMEenvironment variable and add it to the execution path. ThePNPM_HOMEsetting determines wherepnpmwill install packages. - Enable Corepack.
- Use
pnpmto install the Turborepo (theturbopackage).
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.