Dockerizing a NextJS app

06 Feb 2022

This is the first article of a three parts blog post. We will be deploying a custom NextJS app to AWS ECS with Fargate.

TL;DR: You can get all the script/stack in my GitHub repos. Add a star if it helped you, I will keep it updated if other are using it !

The stack

The goal is to deploy a full NextJS stack with NGINX in front on AWS Elastic Container Service (ECS).
We are going to dockerize the app part (NextJS + NGINX), then we will deploy the images on ECS with Fargate in front of it, finally we will set up the CI.

infra schema

Setting up the project

It will be quite minimale. We are going to start from a blank slate.

npx create-next-app@latest

Our final project structure will be the following :

 pages/
 public/ <- NextJS files
 ... 
 Dockerfile <- NextJS Dockerfile
 nginx/ <- Nginx conf folder
  Dockerfile <- Nginx Dockerfile
  default.conf <- Nginx configuration

Dockerizing NextJS

Now let’s create the Dockerfile copied from the official repos. I made some tiny updates, feel free to start from NextJS !

# Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3 
# to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json ./ 
RUN npm ci

# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size 
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
ENV NEXT_TELEMETRY_DISABLED 1

CMD ["node", "server.js"]

Before building this let’s get through this DockerFile ! There are 3 main stages :

Stages allow you to keep your image as tiny and optimised as possible. You can read more here

FROM node:16-alpine AS dips

The docker env will be an alpine linux with node 16 installed.

RUN npm ci

The ci command is used in order install the deps, it allows us to assure that it will be a clean install.

COPY --from=deps /app/node_modules ./node_modules

If by now you are not familiar with multistage build (go read the documentation I linked above) this command copies the node module installed in the deps stage.

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

To increase the security we are creating a nodejs group with a nextjs user.

USER nextjs

Our container will run as the nextjs user.

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./

The file tracing helps reduce the size of deployments and will create a minimal server.js file.
Warning: You must add outputStandalone: true to your next.config.js.

EXPOSE 3000
ENV PORT 3000

CMD ["npm", "start"]

It exposes the 3000 port and run the NextJS app on this port.

Now that we know what we are doing, we can build the container and run it !

docker build -t nextjs-docker .
docker run -p 3000:3000 nextjs-docker

And voila ! We can access our NextJS’s app at localhost:3000

Dockerizing Nginx

We will create a nginx folder that will contain our nginx Dockerfile and the main configuration.

 nginx/
   Dockerfile
   default.conf

Now we can add a minimal nginx configuration.

upstream nextjs-app {
  server nextjs-docker:3000;
}

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  server_name _;
  
  location / {
    proxy_pass http://nextjs-app;
  }
}

Let’s create the Dockerile, here are some great ressources in order to Dockerize our nginx !

FROM nginx:alpine

# Remove the default conf
RUN rm /etc/nginx/conf.d/default.conf

# Copy our custom nextjs nginx conf
COPY ./*.conf /etc/nginx/conf.d/

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

Pretty much it will delete the default configuration, paste all .conf files from our nginx folder and will expose and run Nginx on port 80.

We can now build the Nginx docker image :

docker build -t nginx-nextjs .

Finally we have to create a docker-compose.yml in order to run it all !

At the root folder create the docker-compose.yml file.

version: '3.9'
services:
  nextjs-docker:
    build: ./
  nginx-nextjs:
    build: ./nginx-nextjs
    ports:
      - 80:80

Bind the nginx 80 port in order to encapsulate the nextjs app.

Now we can finally run our app !
Note: If you did not build the image before you can do it docker-compose up –build

docker compose up

That’s it ! You can now access your NextJS app at http://localhost:80

In the next post we will deploy this app on ECS with Fargate.