Continuously Deploying Next.js Website to DigitalOcean Using GitHub Actions

Updated on · 7 min read
Continuously Deploying Next.js Website to DigitalOcean Using GitHub Actions

If you've deployed a Next.js website to Vercel before, you'll understand the allure of its simplicity. Just push your changes to the deployment branch, and within minutes, your updates are live. However, using Vercel as the hosting provider might not be suitable for everyone. Factors such as personal preferences, cost considerations, or a need for flexibility can make alternative hosting platforms more attractive. In such cases achieving the same level of automation can be challenging.

In this guide, we'll explore how to establish a continuous deployment pipeline for a Next.js website on DigitalOcean, utilizing GitHub Actions to create a workflow. This workflow will automatically deploy code changes to a remote DigitalOcean server every time we push to the main branch. While the steps outlined are universally applicable to any remote server, our focus will be on DigitalOcean, as this site is hosted there.

To streamline the process, we won't employ Docker. Instead, we'll leverage the Rsync package to transfer files to the server. By the end of this guide, you'll gain a comprehensive 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.

Setting up an SSH key

Our first task involves generating an SSH key on the server, which GitHub Actions will utilize to establish a connection and transfer files during deployment. To create the SSH key, we'll SSH into the server and navigate to the ~/.ssh directory - the conventional storage location for SSH keys.

If this directory is not present, you'll need to create it. However, remember to avoid logging in as a root user, as this presents a significant 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):

For ease of recognition, we'll rename the key to gh_actions. During the process, the prompt might ask for a key passphrase, which can be left blank. For more details about SSH key passphrases, refer to the GitHub Actions documentation.

If the process is successful, you'll see confirmation 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

With the newly generated key in place, it's now possible to log into the server.

The final step involves copying the generated private key to the clipboard. This will be essential when establishing the secrets for GitHub Actions.

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

The above command prints the private key to the terminal, enabling you to copy it to your clipboard.

Setting up secrets to enable GitHub Actions deployment

In order to allow GitHub Actions to establish a connection with our server without revealing sensitive information, we'll house this data in GitHub Actions secrets. Follow these steps to set them up:

  1. Navigate to your GitHub repository and select the Settings tab.
  2. Locate the Secrets and actions dropdown and click to expand it.
  3. Click on the Actions tab, which takes you to the Actions secrets and variables page.
  4. Towards the top left, find and click on the New repository secret button to initiate the creation of new secrets.

You'll need to create the following secrets:

  • SSH_KEY: The private SSH key we copied earlier.
  • HOST: The domain name or IP address of the deployment server.
  • USERNAME: The user for whom the SSH key was created.
  • TARGET_DIRECTORY: The destination for the deployed code. For DigitalOcean, this typically is /var/www/yoursitename.

After creating these secrets, we'll have gathered all the necessary information to establish the deployment workflow.

Establishing the GitHub Actions workflow

The centerpiece of the deployment process is the GitHub Actions Workflows. These automated procedures execute one or more jobs based on the configurations in a YAML file. This file resides in the .github/workflows directory of the respective repository.

To create a workflow for our website, we'll generate a deploy.yml file within the .github/workflows directory of our website's repository. The file will follow a standard YAML structure, starting with the addition of the workflow name and detailing when it should 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 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@v4 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@v4 with: fetch-depth: 0 ref: main

Next, we'll need to set up Node.js, as our Next.js website is built using it. We'll use the actions/setup-node action to install Node.js on the runner. The node-version parameter specifies the version of Node.js to install. In this case, we'll use the latest LTS version, which is 20.x at the time of writing.

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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.x"

With Node set up, we can proceed to install the dependencies and build the Next.js website. We'll use the npm install and npm run build commands to achieve this.

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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.x" - name: Install Dependencies run: npm install - name: Build Website run: npm run build

The workflow stores the output, which is required for pushing to our DigitalOcean server. However, before executing this step, we need to configure the SSH key, enabling the workflow to connect with the server. Using shimataro/ssh-key-action here will automate a large chunk of the process. This action allows us to add the SSH key to the runner, which is necessary for establishing a connection with the server. Furthermore, we'll also include the remote host in our list of known hosts. This will prevent the workflow from asking for confirmation when connecting to the server. The known_hosts parameter is set to unnecessary to prevent the action from adding the host to the known hosts file. This is because we'll add the host manually in the next step.

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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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@v4 with: fetch-depth: 0 ref: main - name: Use Node.js uses: actions/setup-node@v4 with: node-version: "20.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 powerful yet straightforward tool with many options. It operates with efficiency, updating files by transmitting only the differences between the origin and destination files, thereby conserving bandwidth. For deploying our files, we'll use the following options:

  • a - Activates archive mode, which is a shorthand for the -rlptgoD option. A detailed explanation of these options is available.
  • v - Engages verbose mode, displaying the process progress among other details.
  • z - Compresses file data during the transfer.
  • --delete - Removes all files in the destination that aren't found in the source. Without this option, rsync retains the old version of renamed files, resulting in duplicates. This can lead to problems, especially when renaming configuration files. However, this option also deletes server-specific files, such as .env, rendering it unsuitable for certain use cases.

From this point onwards, each push of code to the main branch will trigger the deployment action, building the code and transferring it to our DigitalOcean server. The progress of this action can be tracked within 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, deploying a Next.js website to Vercel is relatively straightforward. However, some developers may opt for alternative hosting platforms. This article delivers a comprehensive, step-by-step guide to automating the deployment workflow for a Next.js website hosted on DigitalOcean using GitHub Actions. By following the guidelines outlined in this article, developers can streamline their deployment process, making it faster, more efficient, and less susceptible to errors.

In this article we covered the generation of an SSH key on the server, the addition of secrets to activate GitHub Actions deployment, and the establishment of a GitHub Actions workflow. This workflow facilitates the deployment of code modifications to a remote server each time changes are committed to the main branch. By following this tutorial, you now have a functioning continuous deployment pipeline, customizable to your specific needs.

References and resources