back to posts

August 26, 2024

Creating a Lightweight Blog with Next.js: Lessons from Docker to Vercel

How I optimized my blog's deployment with Next.js standalone output and a switch to Vercel.

In my post "Renewing My Blog: The Journey from 2020 to Now” I mentioned about renewing my blog and the challenges I faced, including creating a lightweight Docker image to run on my limited server. Today, I'll share my experience with the Next.js standalone output with docker and the solution I had for my blog.

What is the standalone output?

At a high level, the standalone output is just a folder containing all the necessary files to run your Next.js application.

Some documentation context

According to the Next.js documentation:

During a build, Next.js will automatically trace each page and its dependencies to determine all of the files that are needed for deploying a production version of your application.

By setting the following configuration in next.config.js , Next.js creates a .next/standalone folder and a server.js file, which can be run directly with node server.js.

module.exports = {
 output: 'standalone',
}

However the standalone folder doesn't include the public and .next/static folders. Next.js recommends that these should be handled by a CDN. Since I don't need a CDN for my blog I opted to copy these folders manually to the standalone folder.

Just as a context, my public folder has all images I use on my posts and the static folder contains generated minified JavaScript files, CSS files, images, and other assets. These don't change frequently and that's why Next.js recommends putting these files on a CDN.

Automating the process

To avoid manually moving these folders every time I build the application, I created 2 solutions:

  1. A Bash Script: I can test building it locally, move the folders and run the application. The final script I added to my package.json .
  2. Dockerfile configurations: to move the folders while creating the image.

1. The Bash Script

The bash script move-files.sh runs the build and copies these folders to the .next/standalone folder and it runs the server at the end.

#!/bin/bash


echo "Build"
npm run build


echo "Copying public folder"
cp -r ./public ./.next/standalone/public


echo "Copying static folder"
cp -r ./.next/static ./.next/standalone/.next


echo "Run node server"
node ./.next/standalone/server.js

On the package.json file I added a new script which calls the bash script, called buildStandalone .

 "scripts": {
   "dev": "next dev",
   "build": "next build",
   "start": "next start",
   "lint": "next lint",
   "test": "jest",
   "new-post": "./scripts/new-post.sh",
   "buildStandalone": "./scripts/move-files.sh"
 },

Now I just need to run npm run buildStandalone to build and run my application.

2. The Dockerfile

For my Dockerfile I created 2 stages:

  • Build Stage: It contains everything that Next.js needs to install the dependencies and build the application as usual.
  • Production Stage: Includes just the necessary files for production, in my case just the standalone folder + public and static folders.

Separating in stages it will keep just the latter. Here's the Dockerfile:

# Build the application
FROM node:22-alpine3.19 as BUILD_IMAGE
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build


# Production
FROM node:22-alpine3.19 as PRODUCTION_IMAGE
WORKDIR /app


# Copy just standalone build files
COPY --from=BUILD_IMAGE /app/.next/standalone ./


# Copy public folder from root to the standalone root
COPY --from=BUILD_IMAGE /app/public ./public


# Copy static from root .next to standalone .next
COPY --from=BUILD_IMAGE /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

Comparing Image Sizes

By using the standalone output, I reduced the Docker image size from 1.54GB to 285MB.

The Dockerfile without using the standalone approach:

# Build
FROM node:22-alpine3.19
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build


EXPOSE 3000
CMD ["npm", "run", "start"]

And this is the image with the standalone approach:

Why I'm not using Docker

Although the Docker setup worked totally fine, I need to have a structure to run a container on my server. My server (1GB memory, 25GB disk and 1vCPU) struggled with high CPU and memory usage when running a Docker container. I think due to some read/write operations that the container does, and some system service that collects logs.

Well I didn't dig so much deep into this topic. I noticed that one day my blog was down, and I had to investigate the issue. I saw the monitoring graphs on my server and noticed the high CPU and Memory. I removed everything docker related from my server, cleaned some folders and decided to ditch this approach.

Since I want to keep things simple, I decided to use a more pragmatic solution for my case: deploying to Vercel. Vercel it's free for "hobby” projects, and Next.js is built to be deployed and run on Vercel, I already knew that but I wanted to try it out myself on deploying on my own server.

Conclusion

Using standalone output on Next.js can be beneficial when creating a lightweight Docker image, but it requires some additional setup. It's important to understand your server's resources when you have to run the image in a Docker container to not run out of resources and have it well monitored. You need to decide the best approach for the situation you're facing depending on the whole context and allocate the right time and effort for it.

For my situation, Vercel turned out to be the most efficient and low-maintenance option.

References:


If you're still here. Thanks for reading :)

back to posts