Node.js 20, Yarn 4, and Next.js on Docker

I recently spent time updating a few Swarzy websites including Psalmlist and (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) => {
      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

# 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

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:

# 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

# 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


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.

Node.js 20, Yarn 4, and Next.js on Docker Read More »

Babel 7 dynamic config – making Babel work with esm, Jest, JSX, and Preact server-side rendering

Babel enables use of modern JS code (ES2015 — ES2018) in browsers and environments that don’t inherently support it using a technique known as transpiling. One of the features I enjoy about Babel 7 (currently at beta 46) is the ability to provide a dynamic .babelrc.js configuration file rather than a statically defined .babelrc (JSON) file.

I recently adopted esm (for ES Modules support in Node.js; import and export) on the Verses for Life website.  One of the challenges I ran into with using esm was getting my server to use esm by default for development and production work, but still having my tests work.  (For general esm setup, see esm on npm.)

I’d already decided in the past to use Jest, but, unfortunately, Jest doesn’t play nicely with esm by default and Jest doesn’t support a --require flag like many other JS testing libraries.  So what to do?

The solution is allowing Jest to use Babel to transpile the modules during testing (so you avoid those pesky “unexpected token import” errors), but keeping esm in place for all your other dev and production work.

I set up my Babel 7 config (.babelrc.js) to use Babel to handle modules only in test mode, but esm does the work everywhere else.

.babelrc.js code

This approach enabled my tests to work again.  The downside of this approach is that the way modules are loaded in testing doesn’t match my other environments.  You’ll want to ensure you have appropriate integration tests (a la Puppeteer) to ensure your site works end-to-end.

If you’re early on in deciding your tooling, you can look at the list of the many other test libraries supported by esmand maybe you won’t need this work-around.

Babel 7 dynamic config – making Babel work with esm, Jest, JSX, and Preact server-side rendering Read More »

Installing, Configuring, and Using the XHP PHP Extension by Facebook

Updated June 23, 2012 to add a doctype

Facebook announced the release of XHP yesterday. It is an extension for PHP which provides enhancements to the PHP language syntax and gives the language some convenient capabilities.

In this post I will provide you links to the essential sources of information about XHP, show some XHP sample code, and discuss the installation process for XHP on Ubuntu with Apache and PHP5.

The Essential Information
Download XHP at GitHub
How XHP Works
Building XHP
Configuring XHP

Installing, Configuring, and Using the XHP PHP Extension by Facebook Read More »