My Own Vercel: How I Deploy This Website

Learn how I made my own Vercel-like preview deployments for this website.

Aug 29 2024

This site is built with Next.js, a React framework that’s great for server-side rendering, which boosts load times by generating content at build time and improves SEO. Next.js also provides automatic image resizing, which is really useful for handling different screen sizes.

I’m using the output: standalone option in my build process, which bundles everything into a single package, including a server.js file that handles image resizing and serves all assets. I package all of this into a Docker image and run it on my Kubernetes cluster.

Initially, I hosted the site on Vercel, but I wanted more control, so I moved everything to my own K3s-based Kubernetes cluster. (If you’re curious about how I set up the cluster, I wrote a blog post about it here.)

🏗️

Build Next.js

Build Next.js app in a builder step

🚚

Move Build

Move the resulting Next.js build to the actual image

🏠

Upload Image

Upload the image to home Docker registry tagged with commit SHA

Apply Helm

Apply Helm release using Helmfile with commit SHA as image tag

Optimizing the Docker Image

A major concern when building a Docker image is size. If you go with a standard non-alpine based Node.js base image and just install all dependencies, you’ll end up with an image that’s over 1.6GB. That’s way too big for production.

Thankfully, Next.js offers a template to slim down the image. I use the node:20-alpine image, which is significantly smaller (116MB) compared to the default Debian image (around 1.1GB). Then, I follow a multi-stage build process:

  1. Build the Next.js project in the first stage.
  2. Copy only the necessary production files into the final image.

This approach results in a much smaller image—about 214MB, which is reasonable for production.

Docker Image Size

Here’s the Dockerfile I use:

How I Deploy It

For deployments, I use a combination of:

  • GitHub Actions: This handles the automation, triggering builds and deployments whenever there’s a commit or PR.
  • Helm & Helmfile: These manage the Kubernetes deployments. Helmfile lets me declaratively define everything I need for the deployment, so I can reuse Helm charts easily. A post about my personal helm chart repository is coming next!

Here’s a simplified version of the process:

  1. The Next.js build is created in a builder stage.
  2. The production build is copied into the final Docker image.
  3. The pipeline pushes the image to my private Docker registry, tagged with the commit SHA.
  4. The Helmfile applies the release to my cluster, using the image tag from the commit.

This is the helmfile I use:

Making it More Like Vercel

One of the things I liked about Vercel was the automatic preview deployments for pull requests (PRs). When you open a PR on Github, Vercel automatically deploys the changes and gives you a preview link. I wanted to bring that feature to my setup too!

So, I set up GitHub Actions to do the following:

  1. When a new PR is opened, GitHub Actions builds the Docker image as described above.
  2. The image is pushed to my private registry.
  3. The site is deployed with a unique release name and URL based on the PR number with the help of environment variables in the helmfile.
  4. Github Actions adds a comment to the PR with the deployment URL.Github PR Comment

Here’s how it looks in the GitHub Actions workflow:

This setup mimics the PR-based preview feature of Vercel, but with full control over my infrastructure. When the PR is merged or closed, the deployment is cleaned up automatically.