Project Overview
This is a minimal Node.js/Express backend server. The application itself is straightforward — a single Express 5 app (index.js) that listens on port 3000 and returns "Hello World in AWS EC2!" at the root route. The real substance of the project lies in its deployment infrastructure.
CI/CD Pipeline
The pipeline is split across two GitHub Actions workflows that chain together to form a complete automated delivery process from code commit to live deployment on AWS EC2.

There are two parts of these settings. In your root directory, create a file named Dockerfile
`
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY . .
RUN npm install
EXPOSE 3000
RUN ["chmod", "+x", "./entrypoint.sh"]
ENTRYPOINT [ "sh","./entrypoint.sh" ]
`
For best practice, run your project using the entrypoint.sh:
#!/bin/bash
node index.js
then in order to set DOCKER_HUB_TOKEN & DOCKER_USER_NAME ,
- First register on https://app.docker.com
Create a repository named the same as Github repository.
Go to settings and generate a new access token and copy that token.
Go to your created GitHub repository settings section. In the secret and variable section, you will find Action and create a repository secret key named DOCKER_HUB_TOKEN --> paste your copied dockerhub tocken. In the same way, create another variable named DOCKER_USER_NAME --> copy the username of your Dockerhub account and paste here.
Stage 1 — Build and Publish to Docker Hub (docker_registry.yml)
This workflow triggers on every push to the main branch.
(docker_registry.yml)
`
name: Publish Docker Image to Docker Hub
on:
push:
branches: ['main']
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Login to docker hub
run: echo "${{secrets.DOCKER_HUB_TOKEN}}" | docker login -u "${{secrets.DOCKER_USER_NAME}}" --password-stdin
- name: Build Docker Image
run: docker build . --file Dockerfile --tag "${{secrets.DOCKER_USER_NAME}}"/study-abroad-agency-server:latest
- name: Push Docker Image
run: docker push "${{secrets.DOCKER_USER_NAME}}"/study-abroad-agency-server:latest
`
It runs on a GitHub-hosted Ubuntu runner and performs three steps: it checks out the code, logs into Docker Hub using stored secrets (DOCKER_HUB_TOKEN and DOCKER_USER_NAME), builds a Docker image from the Dockerfile, and pushes it to Docker Hub tagged as latest.
The Dockerfile itself is lean — it uses the node:18-alpine base image, copies the application files, installs dependencies via npm install, exposes port 3000, and delegates startup to entrypoint.sh, which simply runs node index.js.
Finally, you need to set up your newly created AWS EC2 instance. Get access to your instance. Install Docker on your EC2 instance by following https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04.
Following this documentation, you can install Docker on your EC2 server. Then:
using the below command logIn to your Docker that is installed on your EC2 server.
sudo passwd ${USER}
Set a password for using Docker on your EC2 for administration(don't need to use sudo for executing commands)
Now, before you get into the next stage. In your GitHub project repository, go to the Settings ** section, find the **Action from the sidebar, click on Runners, and create a new self-hosted runner.
Then run all of those commands that you found from the GitHub self-hosted runner section on your EC2 instance.
Note: when you run the last command from GitHub self-hosted
./run.sh
This will accept your code as long as the terminal is open. If you want that functionality all the time, whenever you push to GitHub, the runner will accept the code and execute the command.
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
You will see listening for job.
Stage 2 — Deploy to AWS EC2 (awsec2deploy.yml)
This workflow is triggered not by a git event, but by the completion of the first workflow (workflow_run trigger).
(awsec2deploy.yml)
`
name: Publish Docker Images to AWS EC2
on:
workflow_run:
workflows: ['Publish Docker Image']
types: [completed]
jobs:
build:
runs-on: self-hosted
steps:
- name: Stop the old container
run: docker stop study-abroad-agency-server || true
- name: Delete the old container
run: docker rm study-abroad-agency-server || true
- name: Delete the old image
run: docker rmi ${{secrets.DOCKER_USER_NAME}}/study-abroad-agency-server:latest || true
- name: Pull the image from dockerhub
run: docker pull ${{secrets.DOCKER_USER_NAME}}/study-abroad-agency-server:latest
- name: Run the image
run: docker compose up -d
`
This is the key architectural decision — it creates a sequential dependency between the two pipelines, ensuring deployment only proceeds after a successful image publish.
This workflow runs on a self-hosted runner, meaning GitHub Actions is executing directly on the EC2 instance itself. This is how it achieves direct deployment without needing SSH or additional tooling like CodeDeploy. The steps follow a clean replace-and-redeploy pattern: stop and remove the old container, delete the old image, pull the fresh latest image from Docker Hub, and finally bring the container back up using docker compose up -d.
The docker-compose.yml is minimal — it references the published image, names the container, and uses network_mode: host so the container shares the EC2 instance's network stack directly, making the app accessible on port 3000 without port mapping.
Runner will show an error "no configuration file provided" when deploying on EC2. This is for not having the compose file. On your EC2 root.
sudo nano docker-compose.yml
It will open an input terminal push the below code on that file
services:
app:
image: shongkor/study-abroad-agency-server:latest
container_name: study-abroad-agency-server
network_mode: 'host'
for save --> ctrl + X then Ctrl + c
End-to-End Flow
In summary, a developer pushing to main sets off the following chain:
_Git push → GitHub Actions builds Docker image → Pushes to Docker Hub → EC2 self-hosted runner pulls new image → Restarts container via Docker Compose
This is a practical and cost-effective CI/CD setup for a single-server deployment. _
The main trade-off is that network_mode: host and a self-hosted runner introduce some operational coupling to the specific EC2 instance, which would need to be revisited if the architecture scaled to multiple instances or required zero-downtime deployments.
Github Code link : [https://github.com/Shongkor/study-abroad-agency-server]


Top comments (0)