In-Class Activity: Add CD to the Scaffold Project

Goal for Today

By the end of class, we want this example project to support Continuous Deployment.

That means:

CI still runs tests automatically
A successful push to the deployment branch triggers deployment
The server updates automatically
The app restarts automatically
The new version is live without us logging in and doing all steps by hand

We are not trying to build a perfect production DevOps pipeline today.

We are building a first working CD pipeline that is:

Understandable
Small
Correct
Easy to adapt to your projects

Important Firewall Note

Our deployment server is:

10.192.145.179

This server is behind a firewall.

That means the original SSH-based CD plan will not work from a normal GitHub-hosted runner, because GitHub cannot directly SSH into the server.

So we will use a different strategy:

A GitHub Actions self-hosted runner will run on 10.192.145.179.

That runner sits inside the firewall, so it can update the local project directly.

This replaces the original idea where “GitHub connects to the Linux server over SSH” .

What CD Means Here

In this class, CD means:

Code is pushed to GitHub
GitHub Actions runs the test pipeline
If the tests pass, GitHub sends the deployment job to the self-hosted runner
The runner updates the deployed copy of the project
It installs dependencies if needed
It builds the frontend
It restarts the backend server with PM2
The updated app is live

For this scaffold, the backend serves the built frontend, so deployment is naturally a single-server deployment.

Part 1: Start from the Test Setup Branch

We are starting with the branch that already has:

Backend tests
Frontend tests
Playwright smoke test
GitHub Actions CI

Create a new working branch for CD:

git checkout testinclass1
git pull
git checkout -b cdsetup

We will do our CD work on:

cd setup

Part 2: Decide the Deployment Strategy

Because the server is behind a firewall, we will not deploy by SSHing from GitHub into the server.

Instead, we will:

Install a self-hosted GitHub Actions runner on 10.192.145.179
Keep one deployed copy of the repo on the server
Let the runner run the deployment script locally
Update the copy with git pull
Rebuild the frontend
Restart the app with PM2

Flow:

Tested code → self-hosted runner → update deployed repo → rebuild → restart

That is exactly the right level for a first CD activity.

Part 3: One-Time Server Preparation

These steps happen once on the deployment server.

1. Choose the Deployment Directory

Pick a permanent location on the server.

For example:

/home/YOUR_USERNAME/sd/Demo-Scadfold-project

Use one location and stick with it.

2. Clone the Repository Onto the Server

On the server:

cd ~/sd
git clone git@github.com:YOUR_USERNAME/YOUR_REPO.git Demo-Scadfold-project
cd Demo-Scadfold-project
git checkout cdsetup

Do not use sudo for this clone unless you have a very specific reason.

The project should normally be owned by your Linux user.

3. Make Sure .env Exists on the Server

Your deployed app still needs the server-side .env file.

For example:

cd ~/sd/Demo-Scadfold-project/server
cp .env.example .env
nano .env

Set the correct values, including your assigned 41xx port.

Example:

PORT=4101

Use your own assigned port.

4. Install Dependencies and Test Manually

From the project root:

cd ~/sd/Demo-Scadfold-project
npm install

Build the frontend:

cd client
npm install
npm run build

Install backend dependencies:

cd ../server
npm install

Start the server manually:

npm start

Now open:

http://10.192.145.179:41xx

Replace 41xx with your assigned port.

If this does not work manually, stop and fix it before automating deployment.

5. Put the App Under PM2

PM2 is already installed on the server.

From the server directory:

cd ~/sd/Demo-Scadfold-project/server
pm2 start index.js --name demo-scaffold
pm2 save

Now check:

pm2 list

Your process should be there.

This gives us a stable target for automated restart.

Part 4: Create a Deployment Script

Create this file in the root of your project:

deploy.sh

Put this in the file:

#!/bin/bash
set -eecho "Starting deployment..."git checkout cdsetup
git pull
echo "Installing root dependencies..."
npm installecho "Building frontend..."
cd client
npm install
npm run build
echo "Installing backend dependencies..."
cd ../server
npm install
echo "Restarting app with PM2..."
pm2 restart demo-scaffold
echo "Deployment complete."

Make it executable:

chmod +x deploy.sh

Test it manually:

cd ~/sd/Demo-Scadfold-project
./deploy.sh

Refresh your browser and make sure the app still works.

Part 5: Install the Self-Hosted Runner

In GitHub, open your repository.

Go to:

Settings → Actions → Runners → New self-hosted runner

Choose:

Linux

Choose architecture:

x64

GitHub will show you commands.

On 10.192.145.179, run the commands in your home directory.

Recommended location:

mkdir -p ~/actions-runner
cd ~/actions-runner

Then follow the GitHub instructions.

When GitHub asks for labels, you may accept the defaults or add this label:

cd

When setup is complete, start the runner:

./run.sh

Leave it running while testing.

For today, it is fine to run the runner manually.

Part 6: Decide When Deployment Should Happen

For this class, deployment should not happen on every branch.

Use:

cdsetup

while learning.

Later, you can change the workflow to deploy from:

main

For today, we will deploy from:

cdsetup

Part 7: Add the CD Workflow

Create this file:

.github/workflows/cd.yml

Use this workflow:

name: CD

on:
workflow_run:
workflows: ["CI"]
types:
- completed

jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'cdsetup' }}
runs-on: self-hosted

steps:
- name: Deploy on self-hosted runner
run: |
cd ~/sd/Demo-Scadfold-project
./deploy.sh

This version says:

Run CI first
Only deploy if CI succeeds
Only deploy from cdsetup
Run deployment on the self-hosted runner
Update, rebuild, and restart locally on 10.192.145.179

This is the firewall-safe version of CD.

Part 8: Test the Deployment Flow

Make a small visible change.

For example:

Change the page title
Change a line of text on the frontend
Change a heading

Commit and push:

git add .
git commit -m "Test CD deployment"
git push origin cdsetup

Then watch GitHub Actions.

You should see:

CI starts first
CI passes
CD starts after CI
CD runs on the self-hosted runner
The app updates on 10.192.145.179:41xx

This is the live proof that CD is working.

Part 9: What to Watch During Class

In GitHub:

CI starts first
CD starts after CI
Each step is visible
Failure is visible

On the server:

The deployed repo updates
The frontend is rebuilt
The PM2 process restarts

In the browser:

The visible change appears live

That closes the loop.

What Could Go Wrong

1. CD Does Not Start

Check:

Is the CI workflow named exactly CI?
Did CI finish successfully?
Are you pushing to cdsetup?
Is the self-hosted runner online?

2. GitHub Says No Runner Is Available

Go to the runner directory:

cd ~/actions-runner

Start the runner:

./run.sh

Then check GitHub:

Settings → Actions → Runners

The runner should show as online.

3. Deployment Cannot Find the Project Directory

Check the path in cd.yml:

cd ~/sd/Demo-Scadfold-project

Make sure that directory actually exists on the server.

4. git pull Fails

Check:

Is the repo cloned correctly?
Is it on the cdsetup branch?
Does the server have permission to pull from GitHub?
Are there uncommitted local changes blocking the pull?

Useful commands:

git status
git branch
git remote -v

5. PM2 Restart Fails

Check:

pm2 list

Make sure the process name matches the deploy script.

If your PM2 app is named something else, update this line in deploy.sh:

pm2 restart demo-scaffold

6. Frontend Builds Locally But Not on Server

Check:

Did you run npm install in the client directory?
Are all dependencies listed in package.json?
Does npm run build work manually?

7. CD Runs But the Page Does Not Change

Check:

Did npm run build complete successfully?
Is Express serving client/dist?
Are you looking at the correct 41xx port?
Try a hard refresh in the browser.

Suggested AI Prompts for CD

Prompt 1: Explain the Deployment Design

I have a React + Express single-port application.
The backend serves the built frontend.
The server is behind a firewall, so GitHub-hosted Actions runners cannot SSH into it.
I want to use a GitHub Actions self-hosted runner on the server.
Explain this CD design in plain English before giving code.

Prompt 2: Generate the CD Workflow

Write a GitHub Actions CD workflow for a React + Express single-port app.
The app is deployed on a Linux server behind a firewall.
Use a self-hosted GitHub Actions runner.
Deployment should happen only after the CI workflow succeeds on the cdsetup branch.
The deployment command should cd into the deployed repo and run ./deploy.sh.
Return only valid YAML.

Prompt 3: Generate the Deployment Script

Write a deploy.sh script for a React + Express app.
The script should:
checkout cdsetup,
pull the latest code,
install root dependencies,
install and build the client,
install server dependencies,
and restart the app with PM2.
Use set -e so the script stops on errors.

Prompt 4: Debug Deployment Failure

Here is my GitHub Actions CD workflow, my deploy.sh file, and the exact error output.
Tell me the most likely cause and the smallest fix.
Do not propose a major redesign unless necessary.

Prompt 5: Refine Deployment Safety

I already have CI and a simple CD workflow using a self-hosted runner.
Show me how to make deployment happen only after CI succeeds on a specific branch.
Return only the updated workflow YAML and a short explanation.

In-Class Reflection Questions

What is the difference between CI and CD?
Why do we want CD to depend on CI?
Why does the firewall change the deployment design?
Why does a self-hosted runner solve the firewall problem?
Why do we deploy from one branch instead of every branch?
Why is PM2 useful here?
What part of this pipeline is still fragile or simplistic?

Scroll to Top