This gist was partially inspired by this blog about Next.js Vercel CI with GitHub actions.
An easy way to deploy and host websites for free is to use GitHub pages. If you've deployed a Next.js project to GitHub pages, you may have used a GitHub action similar to this in the past to automatically redeploy the site when a new commit is pushed:
# gh-pages-merge.yml
name: Deploy to gh-pages on merge
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- env:
CLIENT_EMAIL: ${{ secrets.CLIENT_EMAIL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
SPREADSHEET_ID: ${{ secrets.EVENTS_APP_SPREADSHEET_ID }}
run: npm ci && CI=false npm run build && npm run export
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: './out'
However, GitHub pages only serves static files, which means you'll have to export your app as static. In the process, you'll lose access to server-only features like server-side rendering, API routes, dynamic routes, and so on.
Unlike GitHub pages however, hosting with Vercel supports a plethora of frameworks and all their advanced, server-dependent features. Best of all, it is completely free for hobby users.
The issue lies with trying to deploy a repository from a GitHub organization. Typically, deploying a project on Vercel only involves importing a git repository, but attempting to import a repository owned by an organization will prompt Vercel to create a team, a feature only available to pro-plan users (at $20/month per user).
If you don't need the advanced features provided by the pro plan and can't afford such a steep cost for a small project, this is a problem.
The idea is to use GitHub actions as our CI and deploy the project through your personal (hobby) subscription via the Vercel CLI. Full docs on the CLI are linked, but all we need to do locally is to create and link the project to Vercel.
Install the Vercel CLI by running
npm i -g vercel
and in your project directory, link it to Vercel by running
vercel
The CLI will prompt you about project details; when asked for a deployment scope, choose your own username. This should initialize the project on Vercel under your personal account, and automatically detect and configure the framework and build presets.
After successfully initializing a project, the CLI will create a .vercel
directory containing a generated project.json
that looks something like this:
{"orgId": "...", "projectId": "..."}
To automatically deploy our project to Vercel on push, we'll use amondnet/vercel-action.
A typical GitHub pages action builds the project to static files and then commits the built files to the gh-pages
branch to be deployed by GitHub pages; for Vercel however, we want to deploy our unbuilt project to Vercel, then let Vercel handle the build and deployment1.
To use vercel-action
you'll need a Vercel Account Token, which you can create here.
When created, add your orgId
and projectId
from project.json
and your account token as repository secrets.
In the GitHub action, pass the secrets we created to vercel-action
2. The workflow file should look something like this:
# vercel-merge.yml
name: Deploy to vercel on merge
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-args: '--prod'
vercel-org-id: ${{ secrets.ORG_ID}}
vercel-project-id: ${{ secrets.PROJECT_ID}}
The action should mimic Vercel's GitHub CI, deploying each new commit to Vercel and commenting on successful pushes.
The vercel CLI can also generate preview URLs for pull requests.
# vercel-pull-request.yml
name: Create vercel preview URL on pull request
on:
pull_request:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: amondnet/vercel-action@v20
id: vercel-deploy
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID}}
vercel-project-id: ${{ secrets.PROJECT_ID}}
- name: preview-url
run: |
echo ${{ steps.vercel-deploy.outputs.preview-url }}
The above workflow will run on all pull requests to main
, generating a preview URL for each that's updated for the latest commit on the branch.
(Note that unlike Vercel's actual CI preview URLs, these URLs are associated with a commit and not an entire pull request. Therefore, the URL will change every time a new push is made to the pull request branch.)
If you're using GitHub secrets to pass environment variables to your project at build time, letting Vercel handle project building means you can no longer do so. Instead, you should set the environment variables within Vercel with dkershner6/vercel-set-env-action.
The vercel-set-env-action
README explains how environment variables should be formatted in the action arguments.
Between the checkout
and vercel-action
, add the env variable setting in your workflow like so:
- uses: dkershner6/vercel-set-env-action@v1
with:
token: ${{ secrets.VERCEL_TOKEN }}
projectName: events-app
envVariableKeys: CLIENT_EMAIL,PRIVATE_KEY,SPREADSHEET_ID
env:
CLIENT_EMAIL: ${{ secrets.CLIENT_EMAIL }}
TARGET_CLIENT_EMAIL: preview,development,production
TYPE_CLIENT_EMAIL: encrypted
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
TARGET_PRIVATE_KEY: preview,development,production
TYPE_PRIVATE_KEY: encrypted
SPREADSHEET_ID: ${{ secrets.EVENTS_APP_SPREADSHEET_ID }}
TARGET_SPREADSHEET_ID: preview,development,production
TYPE_SPREADSHEET_ID: encrypted
The full workflow file should look something like this:
name: Deploy to vercel on merge
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dkershner6/vercel-set-env-action@v1
with:
token: ${{ secrets.VERCEL_TOKEN }}
projectName: events-app
envVariableKeys: CLIENT_EMAIL,PRIVATE_KEY,SPREADSHEET_ID
env:
CLIENT_EMAIL: ${{ secrets.CLIENT_EMAIL }}
TARGET_CLIENT_EMAIL: preview,development,production
TYPE_CLIENT_EMAIL: encrypted
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
TARGET_PRIVATE_KEY: preview,development,production
TYPE_PRIVATE_KEY: encrypted
SPREADSHEET_ID: ${{ secrets.EVENTS_APP_SPREADSHEET_ID }}
TARGET_SPREADSHEET_ID: preview,development,production
TYPE_SPREADSHEET_ID: encrypted
- uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-args: '--prod'
vercel-org-id: ${{ secrets.ORG_ID}}
vercel-project-id: ${{ secrets.PROJECT_ID}}
Footnotes
-
This is contrary to what is stated in
vercel-action
's README about skipping Vercel's build step, but for a Next.js project I found it hard to build locally (in the action) and deploy the built output to Vercel while still having it deploy the project as your selected framework. Easier is to just deploy the whole project and let Vercel build it for you. ↩ -
vercel-action
's README contains more info about additional options you can pass, and the Vercel CLI docs contain more info about flags you can pass tovercel-args
. Here, passing--prod
tovercel-args
deploys the project to a production domain instead of a temporary commit-specific preview URL. ↩