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
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}
:
NEXT_PUBLIC_API_URI=https://api.example.com
And then referencing them with process.env.{VARNAME}
:
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.
# ~ 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