Automate Your Deployment Workflow: Continuously Deploying Next.js Website to DigitalOcean Using GitHub Actions

Updated on · 7 min read
Automate Your Deployment Workflow: Continuously Deploying Next.js Website to DigitalOcean Using GitHub Actions

If you've ever deployed a Next.js website to Vercel, you've likely noticed how easy the process is. You simply need to push your changes to the deployment branch, and within minutes, the updates are live. As software developers, we know that deployment can often be a time-consuming and tedious process, which is why continuous deployment can provide a much-needed improvement in developer experience.

Deploying a Next.js website to Vercel is easy, but not everyone uses it as a hosting platform for various reasons, such as personal preferences, costs, or flexibility. In such a case, you'd still want to keep the same level of developer experience by automating the deployment of your website.

In this post, we'll set up a continuous deployment pipeline for a Next.js website to DigitalOcean. We'll use GitHub Actions to set up a workflow that deploys code changes to a remote DigitalOcean server every time we push to the main branch. Although the steps described here should work for any remote server, we'll focus on DigitalOcean since this site is hosted there. To simplify the process, we won't use Docker but instead, use the Rsync package to transfer files to the server. By the end of this post, you'll have a clear understanding of how to automate your deployment workflow and set up continuous deployment of a Next.js website to DigitalOcean using GitHub Actions.

Here are a few of the options for hosting your website (affiliate links):

  • DigitalOcean - simple and scalable droplets which provide a lot of flexibility.

  • Cloudways - Cloudways is a managed alternative to DigitalOcean.

  • Namecheap - not only a domain marketplace, but also a hosting service for various needs.

Set up an SSH key

First, we'll need to generate an SSH key on the server. This key will be later used by GitHub Actions to connect to the server and transfer the files during deployment. To generate the SSH key let's ssh into the server and change to the ~/.ssh directory, where all the SSH keys are conventionally stored. If for some reason you don't have this directory then you'll need to create it. Also remember not to log in as a root user, since that is a security risk.

bash
ssh username@host
bash
ssh username@host
bash
cd ~/.ssh
bash
cd ~/.ssh

Now that we are in the correct directory, let's generate the SSH key using the built-in ssh-keygen command.

bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

This should start the generation of a new key and will ask for its location.

bash
Generating public/private rsa key pair. Enter file in which to save the key (/Users/YOUR_USER/.ssh/id_rsa):
bash
Generating public/private rsa key pair. Enter file in which to save the key (/Users/YOUR_USER/.ssh/id_rsa):

Here we'll change the name of the key to gh_actions to make it easily recognisable. The prompt will also ask you for a passphrase for the key, which can be left blank. GitHub Actions documentation has more information about SSH key passphrases.

If everything went well, we'd see the success messages in the terminal.

bash
Your identification has been saved in gh_actions Your public key has been saved in gh_actions.pub
bash
Your identification has been saved in gh_actions Your public key has been saved in gh_actions.pub

After this, we will copy the generated public key to the authorized_keys file in the .ssh folder.

bash
cat gh_actions.pub >> ~/.ssh/authorized_keys
bash
cat gh_actions.pub >> ~/.ssh/authorized_keys

Now the newly generated key can be used to log in to this server.

Finally, we need to copy the generated private key to the clipboard. We'll use it when setting up the secrets for GitHub Actions.

bash
cat ~/.ssh/gh_actions
bash
cat ~/.ssh/gh_actions

This command will print the private key to the terminal, from where you can copy it to the clipboard.

Adding secrets to enable GitHub Actions deployment

To enable GitHub Actions to connect to our server without exposing any sensitive information, we'll store the necessary data in Actions secrets. Here's how to add them:

  1. Go to your repository on GitHub and click on the Settings tab.
  2. Look for the Secrets and actions dropdown and click on it to expand it.
  3. Click on the Actions tab to go to the Actions secrets and variables page.
  4. On the top left, you'll see a New repository secret button. Click on it to create new secrets.

We'll need to create the following secrets:

  • SSH_KEY: This is the private SSH key that we copied earlier.
  • HOST: This is the domain name or IP address of the server where the code will be deployed.
  • USERNAME: This is the user for which we've created the SSH key.
  • TARGET_DIRECTORY: This is the destination for the deployed code. On DigitalOcean, it's usually /var/www/yoursitename.

Once we've created these secrets, we'll have all the necessary information to set up the deployment workflow.

Setting up the GitHub Actions workflow

GitHub Actions Workflows is the core element of the deployment process. They are automated processes that run one or more jobs, which are configured by a YAML file. This file is defined inside the .github/workflows directory of a repository to which the actions apply.

To define a workflow for our website, we'll create a deploy.yml file inside the .github/workflows directory of our website repository. The file has a standard YAML structure, and we'll start by adding the workflow name and specifying when it'd run.

yaml
name: Build and Deploy on: push: branches: - main
yaml
name: Build and Deploy on: push: branches: - main

This will define our workflow, named Build and Deploy, and will trigger it every time we push to the main branch of the repository.

Next, we need to add the actual job. The first step of the job will be to check out our main branch and fetch its history. There are predefined actions that make such tasks easier. In this case, we'll use the actions/checkout@v3 action.

yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main
yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main

We'll define a job named build-and-deploy, which will run on the ubuntu-latest runner from GitHub. The first step of the job will be to check out the code from the main branch.

The next step is to set up Node.js for the action, which will be used when installing dependencies and building the website. For this, we'll use another predefined action: actions/setup-node@v3.

yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x"
yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x"

We'll use Node.js version 18 as it's the latest LTS version at the moment.

Now that we have Node setup, we can install the dependencies and build the Next.js website.

yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build
yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build

The workflow will store the output, which we need to push to our DigitalOcean server. Before that, we will need to set up the SSH key for the workflow to be able to connect to the server. For this, we'll use shimataro/ssh-key-action@v2 to help with the setup. Additionally, we'll add the remote host to the list of known hosts.

yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build - name: Install SSH Key uses: shimataro/ssh-key-action@v2 with: key: ${{ secrets.SSH_KEY }} known_hosts: "unnecessary" - name: Adding Known Hosts run: ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts
yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build - name: Install SSH Key uses: shimataro/ssh-key-action@v2 with: key: ${{ secrets.SSH_KEY }} known_hosts: "unnecessary" - name: Adding Known Hosts run: ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts

All the secrets that we have added earlier are stored on the secrets namespace, which we can reference inside the workflow.

Using Rsync for Deployment

Finally, we can push the built files to DigitalOcean using the rsync command.

yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build - name: Install SSH Key uses: shimataro/ssh-key-action@v2 with: key: ${{ secrets.SSH_KEY }} known_hosts: "unnecessary" - name: Adding Known Hosts run: ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts - name: Deploy with rsync run: rsync -avz --delete . ${{ secrets.USERNAME }}@${{ secrets.HOST }}:${{ secrets.TARGET_DIRECTORY }}
yaml
name: Build and Deploy on: push: branches: - main jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Main Branch uses: actions/checkout@v3 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build - name: Install SSH Key uses: shimataro/ssh-key-action@v2 with: key: ${{ secrets.SSH_KEY }} known_hosts: "unnecessary" - name: Adding Known Hosts run: ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts - name: Deploy with rsync run: rsync -avz --delete . ${{ secrets.USERNAME }}@${{ secrets.HOST }}:${{ secrets.TARGET_DIRECTORY }}

Rsync is a simple but powerful tool with many options. It's also very efficient, updating files by sending only the differences and saving bandwidth. We'll use the following options to deploy our files:

  • a - enables the archive mode, which is a shortcut for the -rlptgoD option. There is a detailed list of what they do.
  • v - enables verbose mode, which shows the progress of the process, among other things.
  • z - compresses file data during the transfer.
  • --delete - deletes all files in the destination that aren't present in the source. Without this option, rsync won't delete the old version of renamed files, keeping both. This may cause issues, such as when renaming config files. This option will also delete all server-specific files, such as .env, so it might not be useful in every case.

Now, every time we push code to the main branch, it will trigger the deployment action, build the code and push it to our DigitalOcean server. You can view the progress in the Actions tab of your repository.

If you want to improve the discoverability of your Next.js website, I have written an article about adding a sitemap to it: Add a Dynamic Sitemap to Next.js Website Using Pages or App Directory

Summary

In conclusion, continuous deployment can greatly improve the deployment workflow for software developers. While it's relatively easy to deploy a Next.js website to Vercel, some developers may prefer to use a different hosting platform. This article provides a step-by-step guide on how to automate the deployment workflow for a Next.js website hosted on DigitalOcean using GitHub Actions. By following the instructions outlined in this article, developers can streamline their deployment process, making it faster, more efficient, and less error-prone.

In the article we covered how to generate an SSH key on the server, add secrets to enable GitHub Actions deployment, and set up the GitHub Actions workflow to deploy code changes to a remote server every time changes are pushed to the main branch. By following the steps in the tutorial, you now have a working continuous deployment pipeline, which can be adjusted according to your specific needs.

References and resources

P.S.: Are you looking for a reliable and user-friendly hosting solution for your website or blog? Cloudways is a managed cloud platform that offers hassle-free and high-performance hosting experience. With 24/7 support and a range of features, Cloudways is a great choice for any hosting needs.