Automating Web App Deployment with Terraform, GitHub, and CloudRay

CloudRay makes it easy to automate application deployment across your infrastructure. Instead of juggling manual steps or switching between tools, you can integrate CloudRay with Terraform and GitHub to build a seamless deployment pipeline.

In this tutorial, you will learn how to use Terraform to provision infrastructure on DigitalOcean, and then configure CloudRay to automate the deployment and management of a Node.js application. Additionally, You will also learn how to implement a simple CI/CD pipeline using GitHub Actions and CloudRay webhook, so every push to your repository automatically triggers a deployment.

By the end, you’ll have a fully automated system that provisions a server, deploys your web app, and keeps it updated with zero manual effort.

Contents

Prerequisites

Before you begin, ensure you have the following set up:

  • Terraform installed: You can install Terraform by following the official instructions
  • CloudRay account: Sign up at https://app.cloudray.io
  • DigitalOcean Personal Access Token: Create one via your DigitalOcean control panel. You will need this to authenticate Terraform with DigitalOcean
  • SSH key added to your DigitalOcean account: Create and upload a key using this guide. Make note of the name you assign—it will be used in your Terraform configuration
  • GitHub repository with a sample Node.js app: For this tutorial, we’ll use a sample Node.js app stored in GitHub. You can fork and clone this starter app or use your own

Provisioning Infrastructure with Terraform

Terraform lets you define infrastructure as code and supports a wide range of platforms through installable providers. Each provider acts as a bridge between Terraform and the APIs of the service you’re provisioning—like DigitalOcean in our case.

We will start by using Terraform to provision a virtual machine (droplet) on DigitalOcean. This will serve as the host for our Node.js application.

First, you create a project directory to house your infrastructure configuration files and navigate into the file:

mkdir infra-cloudray && cd infra-cloudray

Next, before running any terraform command, set the following environment variables to pass in your private SSH key and DigitalOcean API token securely. Replace the token value with your own generated token from the DigitalOcean dashboard:

export TF_VAR_pvt_key="~/.ssh/id_ed25519"
export TF_VAR_do_token="dop_v1_XXXXXXXXXXXXXXXXXXXXXXXXXXXX"

TIP

TF_VAR_ prefix allows you to pass environment variables to Terraform as input variables

Create a file named provider.tf which stores the configuration of the provider:

nano provider.tf

Then add the following configuration into the file:

terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

provider "digitalocean" {
  token = var.do_token
}

data "digitalocean_ssh_key" "my_key" {
  name = "my_key"
}

This file sets up the Terraform provider configuration, which tells Terraform to use the DigitalOcean plugin and specifies your authentication token. Replace my_key with the exact name you used when uploading your SSH key to DigitalOcean.

Create the second file named variable.tf:

nano variables.tf

Add the following configurations into the file:

variable "do_token" {
  description = "DigitalOcean API token"
  type        = string
  sensitive   = true
}

variable "pvt_key" {
  description = "Path to the private SSH key"
  type        = string
  default     = "~/.ssh/id_ed25519"
}

This file declares input variables, including the DigitalOcean API token and your SSH private key path.

Finally, create the main infrastructure file named www-cloudray.tf:

nano www-cloudray.tf

Similarly, add the following configuration inside the file:

resource "digitalocean_droplet" "www-cloudray" {
  image  = "ubuntu-24-10-x64"
  name   = "www-cloudray"
  region = "nyc3"
  size   = "s-1vcpu-1gb"
  ssh_keys = [
    data.digitalocean_ssh_key.my_key.id
  ]

  connection {
    host        = self.ipv4_address
    user        = "root"
    type        = "ssh"
    private_key = file(var.pvt_key)
    timeout     = "2m"
  }
}

This is the main infrastructure file where we define our server. We use Ubuntu 24.10 and connect via SSH using your uploaded key. Furthermore, the configuration tells terraform to create a droplet named “www-cloudray”, use the SSH key you added to DigitalOcean and automatically connect connect using SSH for further provisioning.

Now, you initialize the terraform file:

terraform init
Screenshot of successful initiallisation

Next, you run the terraform plan to see your execution plan:

terraform plan
Screenshot of terraform plan output

The + resource "digitalocean_droplet" "www-cloudray" shows that terraform will create a droplet resources named www-cloudray. Then, apply the configuration by running the command:

terraform apply

When prompted, type yes to confirm.

After deployment, you can inspect the created infrastructure and get the server’s IP address using:

terraform show terraform.tfstate
Screenshot of showing server details

Now, you can add the server to CloudRay

Configuring CloudRay for Automated Deployments

Now that your DigitalOcean droplet is provisioned and added to CloudRay, let’s automate deployments using CloudRay’s script orchestration. This section covers:

  • Creating a deployment script and webhook
  • Modifying Terraform to trigger deployment of the application

Let’s get started.

Creating Deployment Script and CloudRay Webhook

First you create the deployment script by following these steps:

To create the setup script, you need to follow this steps:

Screenshot of adding a new deployment script
  1. Go to Scripts in your CloudRay project
  2. Click New Script
  3. Name: Deploy App Script. You can give it any name of your choice
  4. Copy this code:
#!/bin/bash

# Update package lists
sudo apt update -y

# Install Nginx
sudo apt update 
sudo apt install -y nginx 

# Exit on error
set -e

# Install Node.js and npm
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Install PM2
sudo npm install -g pm2

# Clean and clone repo
sudo rm -rf "{{app_dir}}"
sudo mkdir -p "{{app_dir}}"
sudo chown -R $USER:$USER "{{app_dir}}"
git clone "{{repo_url}}" "{{app_dir}}"

# Install dependencies
cd "{{app_dir}}/app"
npm install

# Start application
pm2 start server.js --name nodeapp
pm2 save
pm2 startup

sudo bash -c "cat > /etc/nginx/sites-available/nodeapp" <<EOF
server {
    listen 80;
    server_name {{domain}} www.{{domain}};
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host \$host;
        proxy_cache_bypass \$http_upgrade;
    }
}
EOF

# Enable config
sudo ln -sf /etc/nginx/sites-available/nodeapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

# SSL with Certbot
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d {{domain}} -d www.{{domain}} --email {{email}} --agree-tos --non-interactive
sudo systemctl reload nginx

Here is a breakdown of what each command in the Deploy App Script does:

  • Sets up the web server and runtime environment
  • Clones your Node.js repository and installs dependencies
  • Runs app as a managed background service
  • Routes web traffic to your Node.js app
  • Automatically provisions Let’s Encrypt SSL certificates

before using the scripts, you need to define values for the placeholders {{app_dir}}, {{repo_url}}, {{domain}}, and {{email}}, used in the scrips. CloudRay processes all scripts as Liquid templates. This allows you to use variables dynamically across different servers.

Screenshot of adding a new variable group

To ensure that these values are automatically substituted when the script runs, follow these steps to create a variable Group:

  1. Navigate to Variable Groups: In your CloudRay project, go to “Scripts” in the top menu and click on “Variable Groups”.
  2. Create a new Variable Group: Click on “Variable Group”.
  3. Add the following variables:
  • {{app_dir}}: This is the application install path
  • {{repo_url}}: This is the GitHub repository URL
  • domain: The registered domain name for SSL certificate configuration
  • email: The email address associated with the SSL certificate (used for renewal alerts)

Since the script and variables are setup, proceed to creating a Webhook for the deployment script.

To create an Incoming Webhook in CloudRay follow these steps:

  • In CloudRay navigate and click on “Incoming Webhooks”
  • Click on “New Incoming Webhook” and fill in the detials to create the Webhook
Screenshot of creating a webhook
  • Click “Create New Webhook”. This will generate a unique URL for your webhook
Screenshot of created webhook

Now that your deployment script and CloudRay webhook are ready, the final step is to make sure Terraform can trigger that webhook

Integrating CloudRay Webhook in Terraform

We can update our Terraform configuration to trigger the CloudRay webhook after the droplet is successfully provisioned. Instead of using the remote-exec provisioner, we use a null_resource block with a local-exec provisioner, which runs the webhook call locally from the machine running Terraform.

Update your www-cloudray.tf file:

nano www-cloudray.tf

Add the following null_resource configuration outside the droplet block:

...
resource "null_resource" "trigger_cloudray_webhook" {
  provisioner "local-exec" {
    command = <<EOT
      curl -X POST \
        https://api.cloudray.io/w/58b3b37d-a77c-4b22-a6bc-ef3e2af8af34 \
        -H "Content-Type: application/json" \
        -d '{"test": false}'
    EOT
  }

  depends_on = [digitalocean_droplet.www-cloudray]
}
...

This tells Terraform to trigger the CloudRay webhook only after the www-cloudray droplet has been successfully created.

Your updated www-cloudray.tf file should now look like this:

Screenshot of new wwww-cloudray.tf file

This approach ensures that the webhook is triggered reliably during the provisioning process without the need for SSH access into the droplet.

Before applying your changes, reinitialize your Terraform project and upgrade any modules or providers:

terraform init -upgrade

Then apply the configuration:

terraform apply

Once terraform apply completes successfully, head over to your CloudRay Runlog to confirm that the webhook was received and the deployment job ran successfully.

Screenshot of successful Runlog

Open your browser and navigate to your configured domain (e.g., https://www.mydomain.com). You should now see your application live and running on the newly provisioned droplet.

Screenshot of successful Runlog

Setting Up Continuous Deployment with GitHub and CloudRay

To automate your deployment pipeline, we’ll integrate GitHub Actions with CloudRay so that any push to the main branch triggers a webhook, which then updates your server with the latest code.

First, let’s create the script on CloudRay to update always update the application. You can follow similar process as the above and use this code:

Screenshot of setup CI
#!/bin/bash
set -e  # Exit immediately if any command fails

# Navigate to project root
cd /var/www/nodeapp

echo "➡️ Pulling latest changes..."
git pull origin main

# Move to app directory
cd app

echo "📦 Installing dependencies..."
npm install

echo "🔄 Restarting application..."
pm2 restart nodeapp || (pm2 delete nodeapp && pm2 start server.js --name nodeapp)

Next, we also create a Webhook following similar steps as discussed earlier

Screenshot of setup CI

Then go to the project repository (the node.js application), create a directory and file:

mkdir -p .github/workflows
touch .github/workflows/deploy.yml

Then add the following content to .github/workflows/deploy.yml:

name: Deploy to CloudRay

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger CloudRay Webhook
        env:
          WEBHOOK_URL: ${{ secrets.CLOUDRAY_WEBHOOK }}
        run: |
          curl -X POST "$WEBHOOK_URL" \
            -H "Content-Type: application/json" \
            -d '{"test": false}'

To add your CloudRay webhook to GitHub secrets, follow these steps:

Screenshot of first addition of GitHub secret step
  • Go to your GitHub repo Settings → Secrets → Actions
  • Click “New repository secret”
Screenshot of first addition of GitHub secret step
  • Name: CLOUDRAY_WEBHOOK
  • Value: Paste your CloudRay webhook URL
  • Click “Add secret”

Finally, let’s test the pipeline, make a changes on the application (modify the index.html file):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome</title>
</head>
<body>
    <h1>Greetings from CloudRay</h1>
    <p>This update was deployed automatically via GitHub → CloudRay 🎉</p>
</body>
</html>

Commit and push to main:

git add .
git commit -m "Test CI/CD pipeline"
git push origin main

Watch the process, GitHub Actions will show the workflow running.

Screenshot of first addition of GitHub secret step

Additionally, CloudRay will display the script execution in Run Logs.

Screenshot of first addition of GitHub secret step

Finally, visit your domain to see changes live within seconds.

Screenshot of first addition of GitHub secret step

Conclusion

By following this guide, you’ve successfully built a complete infrastructure automation and CI/CD pipeline that combines Terraform, DigitalOcean, CloudRay, and GitHub Actions.

The entire process from server creation to application updates now happens automatically whenever you push code changes, giving you more time to focus on development rather than deployment tasks.

Ready to streamline your own deployment workflow? Sign up for CloudRay today and experience the power of infrastructure automation firsthand.