Continuous deployment involves frequently integrating code changes into a shared repository. This allows for your production app to be updated with the latest code changes in a continuous release format easily. Several turn-key solutions exist from many different providers. However, in this article, we’ll explore how to roll your own continuous integration system using Node.js, Git, PM2, and Linux.
Prerequisites
A Node.js app that has its source code hosted in a bare git repository on the server. This is different than just cloning the repository. The server is actually hosting the repo. I wrote a guide for this.
The repository has been cloned to another location on the server. This is the production code that PM2 will manage.
The production code for the app is being run and managed by PM2. I also wrote a guide for this.
Set up CI
In the bare repository (not the cloned repository that PM2 is running from), create a post-receive git hook by creating a file named [repo name]/hooks/post-receive
.
#!/bin/bash
while read oldrev newrev refname
do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "master" = "$branch" ]; then
cd /path/to/cloned/app
unset GIT_DIR
git pull
npm ci
npm run build
pm2 reload [app name]
fi
done
On the line if [ "master" = "$branch" ]; then
, master
must be the name of the branch which should trigger CI when it's pushed to. If you use some other name for your master branch, just change it on that line.
On the line cd /path/to/cloned/app
, make sure that path points to the actual cloned production code that PM2 is managing.
On the line pm2 reload [app name]
, the [app name]
must be the name you set in ecosystem.config.js
.
Make the hook executable.
chmod +x [repo name]/hooks/post-receive
The setup is complete. Here is the process this continuous integration solution follows:
A developer pushes changes to the bare repository
The post-receive hook in the bare repository is automatically executed
The hook changes into the cloned production repository and pulls the changes
Any missing dependencies are installed
The production app is rebuilt
The processes are reloaded by PM2, one at a time for zero downtime
All of this happens automatically when changes are pushed to the repository.
Reduce Downtime on a Next.js App
With the current configuration, there will still be some downtime when Next.js builds your app. During the build, it removes everything from the .next
directory, then repopulates it. Since your running processes are accessing files in the .next
directory, the app becomes temporarily unavailable.
You can solve this by having Next.js build into a temporary directory, then moving the built files over once the build is complete.
Edit next.config.js
. Add the distDir
line below.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
distDir: process.env.BUILD_DIR || ".next",
};
This allows us to set a build directory at build time.
Now edit the post-receive
hook again. The changes are...
Modify the build command to set a temporary build directory
.next_temp
Add a command to remove the old
.next
directoryAdd a command to rename
.next_temp
to.next
.
#!/bin/bash
while read oldrev newrev refname
do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "master" = "$branch" ]; then
cd /path/to/cloned/app
unset GIT_DIR
git pull
npm ci
BUILD_DIR=.next_temp npm run build
rm -rf .next
mv .next_temp .next
pm2 reload [app name]
fi
done
Now you have a running app and a bare git repository hosted on the server. When a developer pushes to that repository, the changes are automatically applied and pushed to your production environment.
In this article, we’ve explored how to set up a continuous integration system using Node.js, Git, PM2, and Linux. By leveraging git hooks and PM2’s process management capabilities, we can enable continuous releases of our production app without much hassle.
Cover photo by Omid Armin on Unsplash.