How I integrated a simple CI/CD for Next.js website on a VPS

Abdessamad Ely
Abdessamad Ely
Software Engineer
Updated on   •   Next.js

This tutorial is a follow-up to How I Deployed a Next.js Website on a VPS, where I explain every step needed to deploy a Next.js application from an empty VPS to a live website with HTTPS.

In this tutorial, we’ll continue that deployment journey by focusing on integrating GitHub Actions to automatically deploy the application every time changes are pushed to the production branch.

If you don’t yet have a server, I recommend getting a VPS from Hostinger. I’ve been using it for 3 years, and it’s been working perfectly.

Note: The shared commands were tested on Hostinger’s VPS running “Ubuntu 24.04.3 LTS” operating system.

Creating a production branch

We want to trigger a new deployment each time we push new changes to our production branch, in this tutorial I will use production as the name of the branch.

The main flow will be pushing new commits to the main branch for development, and once we’re ready to deploy then we merge main branch into the production branch.

Here are some GIT cheat sheet for what we’re gonna use:

  • git checkout -b production : create a new branch called production.
  • git checkout main or git checkout production : switch between branches.
  • git branch --show-current : double check current active branch.
  • git add . and git commit -m 'commit message' : stage changes and commit them to the active branch.
  • git push : push changes to the remote repository (e.g. on GitHub).
  • git pull origin main : merge the main branch from remote repository into the production branch.

For now, let’s go ahead and create a new branch using: git checkout -b production

Creating a Deployment Workflow

A workflow is a YAML configuration file that instructs GitHub to automate a task, in our case the task is to deploy our production branch each time we push a new change.

To create a GitHub workflow we need to create a new file inside .github/workflows/ssh-deploy.yml in our project root.

The .github/workflows folder is a convention for GitHub Actions, so the folder name should match exactly for GitHub Actions to recognize it.

The actual configuration filename (ssh-deploy.yml) can be modified to reflect the role of its configuration (as long as the configuration is valid).

Let’s go ahead and setup everything we need, before going back to our workflow configuration file.

Ref: GitHub’s Workflow syntax for GitHub Actions guide.

How GitHub uses SSH keys

In the previous step, when we deployed our application, we used our non-root SSH public key to give our server read access to GitHub repository. We did that through GitHub deploy keys, this allowed us to clone and pull changes using SSH.

Normally, when we want to connect from our computer to a server through SSH, we add our computer’s public key to the server’s authorized_keys. But for a GitHub Actions Job to connect to our server through SSH, it uses the private key instead.

That make sense, because it’s not practical to generate a static SSH keys (or multiple) for every GitHub Actions Workflow.

But for GitHub Actions Job to connect to out server using the private key, we need to add our SSH public key to our server’s authorized_keys.

Let’s do that, so our private key can be trusted:

  • Using cat ~/.ssh/id_ed25519.pub to get our public key content
  • Copy it’s content add insert it at the end of the ~/.ssh/authorized_keys file
    • You can use vim ~/.ssh/authorized_keys , or nano ~/.ssh/authorized_keys

Let’s do that, so our private key can be trusted:

  • cat ~/.ssh/id_ed25519.pub : to get our public key content
    • Copy the content to the clipboard
  • vim ~/.ssh/authorized_keys , or nano ~/.ssh/authorized_keys to edit the authorized_keys file:
    • Append the copied SSH public key to the end of the ~/.ssh/authorized_keys file, then save.

Adding GitHub Secrets

Now that we trusted our SSH key, let’s store it in a safe place GitHub Secrets.

GitHub Secrets are similar to environment variable, we can create as many as we like, but for our purpose we will need the following:

  • SSH_PRIVATE_KEY: The private key of our newly created SSH keys:
    • use cat ~/.ssh/id_ed25519 to get its content.
  • SSH_HOST: IP address or domain name of the VPS (if you use a domain, it must resolve directly to the server’s IP and not be proxied)
  • SSH_USERNAME: The non-root user we created (e.g. kolora).
    • use whoami in case of doubt.

Similar to Deploy keys:

  • Go to your Next.js repository.
  • Open the Settings tab, and search for Secrets and variables, then Actions.
  • Now using the Repository secrets section go ahead and create the secrets we mentioned above.
GitHub Repository Secrets

Ref: GitHub’s Using secrets in GitHub Actions guide.

CI/CD Automation with GitHub Actions

After generating a new SSH keys, creating Deploy keys, and creating the necessary GitHub secrets, it’s time to go back to our .github/workflows/ssh-deploy.yml configuration file.

Let’s write the full configuration, then go through it step by step:

.github/workflows/ssh-deploy.yml
name: Deployment Workflow
on:
  push:
    branches: [production]

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    steps:
      - name: SSH to server
        uses: appleboy/ssh-action@v1.2.4
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            cd /var/www/nextjsproject/kolora.app/
            eval `ssh-agent -s`
            ssh-add ~/.ssh/id_ed25519
            git checkout production
            git pull origin production
            git status
            export PATH=/home/${{ secrets.SSH_USERNAME }}/.nvm/versions/node/{node_version}/bin:/usr/bin:/bin
            npm install && npm run build
            pm2 restart kolora

Using node -v to get your installed node version, then replace the {node_version}, something like:

  • /home/${{ secrets.SSH_USERNAME }}/.nvm/versions/node/v24.12.0/bin:/usr/bin:/bin

Reading the configuration YAML file is straight forward, but let’s go through it just in case:

.github/workflows/ssh-deploy.yml:snippet
name: Deployment Workflow
on:
  push:
    branches: [production]
  • name: we provide name for our workflow, this will be used on GitHub Actions dashboard
  • on, push, branches: Here we tell GitHub Actions to run our workflow when do git push to out production branch
.github/workflows/ssh-deploy.yml:snippet
jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    steps:
      - name: SSH and Deploy
        uses: appleboy/ssh-action@v1.2.4
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |

When working with GitHub Actions, each job runs on its own machine, so here we defined a job called Deploy, which runs on Ubuntu operating system.

The Ubuntu instance is what will connect to our server through an SSH connection.

Then, we define the steps for our Deploy job, which in our situation we only need a single step SSH and Deploy.

  • uses: appleboy/ssh-action: in order to connect to our server from a GitHub Actions Job we’re using a third-party GitHub Action
  • with, host, username, …: these are parameters passed to the ssh-action, and they are required to establish an SSH connection.
  • script: after connecting to our server, this parameter specifies the deployment commands to be executed remotely.

Ref: SSH for GitHub Actions

Deployment script

Because we already deployed our Next.js application manually in the previous tutorial, we know the commands that need to be executed each time there are new changes.

The commands are straightforward, but you can adapt them to meet your project requirements (e.g., run migrations, etc.):

.github/workflows/ssh-deploy.yml:snippet
# Navigate to Next.js project (Absolute path)
cd /var/www/nextjsproject/kolora.app/

# Make sure the ssh-agent is running
eval `ssh-agent -s`

# Make sure our SSH key is added to the ssh-agent (required for Deploy keys to work)
ssh-add ~/.ssh/id_ed25519

# Make sure our production branch is active, then pull new changes from the origin repository
git checkout production
git pull origin production

# Make sure our commands (e.g. npm, pm2, etc) are available
export PATH=/home/${{ secrets.SSH_USERNAME }}/.nvm/versions/node/v24.12.0/bin:/usr/bin:/bin

# Install new dependencies, then build our project
npm install && npm run build

# Finally restart our Next.js server to reflect on new changes
pm2 restart kolora

Conclusion

At this point, you should have clear idea of how you can automate your deployment process for Next.js applications, with this knowledge as a base, you could apply the same thing for other automations.

Abdessamad Ely
Abdessamad Ely
Software Engineer

I am a web developer from Morocco Morocco flag and the founder of abdessamadely.com, a personal website where I create content to help developers grow and improve their programming skills.