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 calledproduction.git checkout mainorgit checkout production: switch between branches.git branch --show-current: double check current active branch.git add .andgit 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.pubto get our public key content - Copy it’s content add insert it at the end of the
~/.ssh/authorized_keysfile- You can use
vim ~/.ssh/authorized_keys, ornano ~/.ssh/authorized_keys
- You can use
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, ornano ~/.ssh/authorized_keysto edit the authorized_keys file:- Append the copied SSH public key to the end of the
~/.ssh/authorized_keysfile, then save.
- Append the copied SSH public key to the end of the
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_ed25519to get its content.
- use
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
whoamiin case of doubt.
- use
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.

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:
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 koloraUsing 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:
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 pushto outproductionbranch
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.
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.):
# 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 koloraConclusion
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.