I recently made the switch from Mac to Windows - I won't get into the reasons why, other than to mutter a few grumbles about keyboards. What I will say is that having our projects well Dockerised proved to be invaluable in making the move. Where previously I'd have lost days to getting my dev environment setup, a simple
docker-compose up dev worked seamlessly, out of the box.
My dev environment isn't the only area where Docker is valuable, of course. While it may seemingly go against convention, we generally choose to deploy our Next.js apps as Fargate services. I won't get into the many Dev Ops related reasons for this, but I will show you how we use Docker and Docker Compose to run our Next.js apps effectively, across environments...
With those prerequisites out of the way, let's start with our
FROM node:current-alpine AS base WORKDIR /base COPY package*.json ./ RUN npm install COPY . . FROM base AS build ENV NODE_ENV=production WORKDIR /build COPY --from=base /base ./ RUN npm run build FROM node:current-alpine AS production ENV NODE_ENV=production WORKDIR /app COPY --from=build /build/package*.json ./ COPY --from=build /build/.next ./.next COPY --from=build /build/public ./public RUN npm install next EXPOSE 3000 CMD npm run start
This may not look like the
Dockerfile you were expecting... This is a "multi-stage" Dockerfile, which can be used for both development and production deploys. There are various reasons you may want to do this, but the primary one is that the size of our docker images can be reduced dramatically as they only bundle the result of the final step.
Let's take a look at that first step:
FROM node:current-alpine AS base WORKDIR /base COPY package*.json ./ RUN npm install COPY . .
This looks more or less like any other node related Dockerfile; it extends from the official node image, copies our package.json and installs it, then adds the working project files.
This next stage is where things get interesting - this is where we compile our next app:
FROM base AS build ENV NODE_ENV=production WORKDIR /build COPY --from=base /base ./ RUN npm run build
Each stage of a multi-stage Dockerfile is self-contained, so we have to explicitly copy any files we want from the base step. This step only relates to a production build, so we're explicitly setting the
production, copying the files from the base step, and running the build script specified in our package.json.
With our app compiled, we're on to the final step: creating a lean, production-ready image.
FROM node:current-alpine AS production ENV NODE_ENV=production WORKDIR /app COPY --from=build /build/package*.json ./ COPY --from=build /build/.next ./.next COPY --from=build /build/public ./public RUN npm install next EXPOSE 3000 CMD npm run start
From the previous build step, we copy our package.json, the
.next directory which contains our compiled app, and the directory which contains our public assets across. Finally, it installs the next package, uses it to start our compiled app, and exposes it at
localhost:3000. The only files this final image contains are the ones that we copied across - the essentials - keeping it super lean. We've ditched our heavy node_modules directory, among other things.
|You may have noticed I specified
That's our Dockerfile done; now how do we run
next dev with this thing?
Simple: we need a
version: "3.7" x-common-props: &common-props build: context: ./ target: base working_dir: /base volumes: - ./app:/base - node_modules:/base/node_modules services: npm: <<: *common-props entrypoint: npm dev: <<: *common-props ports: - "3000:3000" command: npm run dev volumes: node_modules:
This gives me two local services;
dev. Both use the
base step from our
npmcommand as it's entry point, so I can write convenient commands like
docker-compose run npm i -s moment.
devscript from our
package.json, so I can start the whole thing up with
docker-compose up devand see my app running at
I have some
common-props to share attributes between services, and these include mounting a volume for my
node_modules - a useful trick for sharing modules between containers and saving a lot of time.
To sum up: adding these two files to the root of any standard next.js project should have you up and running in no time - with:
- Out of the box, cross-platform development for your whole team.
- Blazing fast, fully containerized production deploys.
If you've any reservations about how performant a containerised next.js app, compared to one targetting serverless, I leave you with this timeline from the dynamic, database driven homepage of one of our projects: