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_HOME
environment variable and add it to the execution path. ThePNPM_HOME
setting determines wherepnpm
will install packages. - Enable Corepack.
- Use
pnpm
to install the Turborepo (theturbo
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.