I recently spent time updating a few Swarzy websites including Psalmlist and Swarzy.com. (Yes, this is how I spent my time off for Veteran’s Day Observed.) I’ve always found that having and working on other websites keeps me fresh and up-to-date on the latest technologies when I’m using them at work.
As part of tech maintenance for a website, it’s important to keep your dependencies updated. And so I embarked on updating my stack to the latest versions of key dependencies. Here’s what I’m using right now:
- Node.js 20.x
- Yarn 4.x
- Next.js 14.x
I’ve found that to get an optimal size for the built Docker container image, it’s helpful to use Next.js output in “standalone” mode.
Next.js output in Standalone mode
To get an optimal build size, I’ve discovered that setting the output to standalone is very helpful. Next.js analyzes the static build output and only includes the Node.js module dependencies that your application actually uses. This helps avoid the need to do a yarn install
using only the production dependencies and ends up simplifying the Dockerfile.
// next.config.js // known to work with Next 14.x, this should work with some versions of Next 13.x) const NODE_ENV = process.env.NODE_ENV; /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', reactStrictMode: true, webpack: (config) => { config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], }); // We upload source maps for production. if (NODE_ENV === 'production') { config.devtool = 'hidden-source-map'; } return config; }, }; export default nextConfig;
Two-Stage Dockerfile for Node.js 20, Yarn 4, and Next.js 14
It has been a journey of discovery finding a pattern for a Dockerfile that plays well with Yarn 4 and works correctly across a multi-stage build. Very few examples of Yarn 2 or 3 exist on the web, and Yarn 4 is so new that this may be one of the first blog posts with an example of how to do it.
For the strategy, what I’m doing is starting the Dockerfile from a good Node.js image: node:20-alpine
and creating a base
that I can use for the subsequent stages. I have a second stage I call builder
(following other examples online) and the final stage called runner
.
In the base, I set a few shared environment variables including the YARN_VERSION
, one for disabling Next.js telemetry, and ensuring that the build and runtime are set up for production (not dev). The base image also includes a few apk
dependencies for the Alpine operating system.
To get Yarn working and available across stages, I enable corepack and set it to Yarn 4 as part of the base
.
The builder
stagecopies the files that are needed from the git repo and executes the build. Because we’re using standalone, we don’t need to separate the dependency installation from the yarn build — we can do those in the same stage and it doesn’t affect the final container image size.
The final stage for runner
takes the built files from the builder
stage, sets appropriate permissions, and copies the relevant directories into place for the Node.js runtime to run the build Next.js website. We’re using dumb-init
to run node server.js
(which is created from the standalone Next.js build).
The Dockerfile for Node 20 & Yarn 4 and Next.js 14
My hope is that this file will be easy to upgrade for future versions of Node.js. Because it’s sharing the base
across stages, using the newest Node.js is as simple as switching out node:20-alpine
for a newer version when it becomes available as the next LTS (Long Term Support) release. So long as Next.js continues to support standalone mode with the same folder structure, the Dockerfile shouldn’t need updated. And updating Yarn is now as simple as changing YARN_VERSION
to the newest version number and let corepack
do what it needs to do.
Here’s the code in all its glory. If you have suggestions for improvements, please reach out and let me know!
FROM node:20-alpine AS base
# Setup env variabless for yarn and nextjs
# https://nextjs.org/telemetry
ENV NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production YARN_VERSION=4.2.2
# update dependencies, add libc6-compat and dumb-init to the base image
RUN apk update && apk upgrade && apk add --no-cache libc6-compat && apk add dumb-init
# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
# add the user and group we'll need in our final image
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Install dependencies only when needed
FROM base AS builder
WORKDIR /app
COPY . .
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
RUN yarn install --immutable
# Add `ARG` instructions below if you need `NEXT_PUBLIC_` variables
# then put the value on your fly.toml
# Example:
# ARG NEXT_PUBLIC_SOMETHING
# Build the app (in standalone mode based on next.config.js)
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
# copy the public folder from the project as this is not included in the build process
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# copy the standalone folder inside the .next folder generated from the build process
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# copy the static folder inside the .next folder generated from the build process
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 8038
ENV PORT 8038
CMD ["dumb-init","node","server.js"]
I hope this example is useful to other people. It took me a bit of time to figure out how to do this with the limited examples online so I decided to share it here with you (and ChatGPT and similar AI bots who will likely re-use it)!
Update May 23, 2024:
Fortunately, this approach works well with the latest versions of each of these technologies. You can update to Node.js v22, Yarn 4.2.2, and Next.js 15 RC (with React 19 RC) without changing the Dockerfile very much at all. For Node.js 22, change the base image to node:22-alpine
. For Next.js on 15 RC, you may need other configuration changes, but the ones above should continue to work for a basic site. Of course, depending on your site, the Next.js 15 and React 19 updates could require other configuration and code changes, but a boilerplate or small site may not require much adjustment.