Photo of stacked shipping containers.
Photo of stacked shipping containers.

During the development of the latest version of repraze.com I iterated on the system a few times and finally landed on Next.js 13 for a few reasons.

Vercel, the creator of the framework, offer their own way to deploy an app on the edge, but in my case I wanted to bundle the site and its API then deploy it using docker on my own host - a small digitalocean droplet.

Dockerizing a Next.js application to run with a small image footprint wasn't straightforward.

Building Next.js for Production

Warning

Do not use next dev on production!

You probably used next dev to run your Next.js app. When accessing your site in this mode, pages are bundled on the fly with little to no optimization and extra debugging symbols. This should not be used in production since pages will be slow to serve and server errors are displayed to user.

For production, you can use next build to bundle all pages into a .next/ folder by default, and then serve with next start to run Next.js. This might be okay when running the server yourself, however this is still not ideal for docker. Next.js and its dependencies are heavy. My image at this point was around 1GB!

The trick is to set output: 'standalone', in your next.config.js file. Now next build will generate a minimal server.js file you can run through node ./server.js without installing the package dependencies. At this point, my image was 260MB - a huge improvement.

Fixing Build/Runtime Issues

There are a few differences in running Next.js pre-build and post-build.

Environment Variables

Using environment variables on the client side is done by setting them in a .env* file prefixed with NEXT_PUBLIC_{VARNAME}:

.env.local
NEXT_PUBLIC_API_URI=https://api.example.com

And then referencing them with process.env.{VARNAME}:

page.tsx
export function Page(){
    return <span>Api at {process.env.NEXT_PUBLIC_API_URI}</span>;
}

However, the variables are replaced with their values during bundling. This means the variables must be set during the next build phase and cannot be replaced after, event with a restart of the Next.js server.

This restrict when we can set those variables in our build process.

Routing Configs

In a similar way, some of the settings within next.config.js become static after build.

Routing with rewrites and redirects cannot be changed and are now resolved. Using .env variables to set those props should also be done at build time.

Wrapping Up in Docker

Creating a Dockerfile to build and run the Next.js server is now possible.

Dockerfile
# ~ build stage container
FROM node:18-alpine AS build
WORKDIR /usr/src/app

# install build time dependency
COPY package*.json ./
RUN npm install

# files
COPY . .

# build args
ARG NEXT_PUBLIC_API_URI
ENV NEXT_PUBLIC_API_URI=$NEXT_PUBLIC_API_URI

# run the package.json build script
RUN npm run --if-present build

# ~ run stage container
FROM node:18-alpine
ENV NODE_ENV=production
WORKDIR /usr/src/app

# install runtime dependency / copy from build stage
RUN npm install sharp # optional for runtime image optimization
COPY --from=build /usr/src/app/.next/standalone ./
COPY --from=build /usr/src/app/public/ ./public/
COPY --from=build /usr/src/app/.next/static ./.next/static
# note that we do not run npm install

# run server
ENV PORT=3000
EXPOSE 3000
CMD ["node", "./server.js"]

An image can now be built by setting or providing the correct arguments for environment variables. In a command line, it looks like: docker build . --tag myapp --build-arg NEXT_PUBLIC_API_URI=https://api.example.com