Rolling Your Own Continuous Deployment with Node.js, Git, PM2, and Linux
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
.nextdirectory - Add a command to rename
.next_tempto.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.
Travis Horn