DEV Community

Cover image for CI/CD Pipeline for a Dockerized App on AWS (From zero to production).
Bala Audu Musa
Bala Audu Musa

Posted on

CI/CD Pipeline for a Dockerized App on AWS (From zero to production).

At the end of the project, you should be able to confidently say to your potential employer:

"I built a CI/CD pipeline using GitHub Actions to automatically test, build, and deploy a Dockerized application to AWS EC2, using Nginx and a blue/green strategy to achieve zero-downtime deployments."

Step 0.1 β€” Create a new project folder
On your local machine:

cd ~/OneDrive/Desktop
mkdir start-project1-cicd-from-scratch
cd start-project1-cicd-from-scratch
Enter fullscreen mode Exit fullscreen mode

Initialize git:

git init
Enter fullscreen mode Exit fullscreen mode

Step 0.2 β€” Create a NEW GitHub repository
Go to πŸ‘‰ https://github.com/new

  • Repository name:
start-project1-cicd-from-scratch
Enter fullscreen mode Exit fullscreen mode

Visibility: Public
❌ Do NOT add README
❌ Do NOT add .gitignore
Create the repo.

Then connect local β†’ GitHub:

git remote add origin https://github.com/dr-musa-bala/start-project1-cicd-from-scratch.git
git branch -M main

Enter fullscreen mode Exit fullscreen mode

Step 0.3 β€” Confirm clean state
Run:

git status
Enter fullscreen mode Exit fullscreen mode

You should see:

On branch main
No commits yet
Enter fullscreen mode Exit fullscreen mode

βœ… This is perfect.

🧱 PHASE 1 β€” Build the Minimal Node App (the β€œdeploy target”)

Goal: Create a tiny app + test so CI has something real to run.
1) Create the project files
From inside start-project1-from-scratch run:

npm init -y
npm i express
npm i -D jest supertest
Enter fullscreen mode Exit fullscreen mode

2) Create app.js.

code app.js
Enter fullscreen mode Exit fullscreen mode

Paste:

const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Project 1: CI/CD from scratch βœ…");
});

app.get("/health", (req, res) => {
  res.status(200).send("OK");
});

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

*3) Create *server.js

code server.js
Enter fullscreen mode Exit fullscreen mode

Paste:

const app = require("./app");

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

4) Create a test file

Create folder + test:

mkdir test
code test/app.test.js
Enter fullscreen mode Exit fullscreen mode

Paste:

const request = require("supertest");
const app = require("../app");

describe("GET /", () => {
  it("should return 200", async () => {
    const res = await request(app).get("/");
    expect(res.statusCode).toBe(200);
  });
});

Enter fullscreen mode Exit fullscreen mode

5) Update package.jsonscripts
Open:

code package.json
Enter fullscreen mode Exit fullscreen mode

Replace the scripts section with:

"scripts": {
  "start": "node server.js",
  "test": "jest"
}

Enter fullscreen mode Exit fullscreen mode

6) Run locally (must pass)

npm test
Enter fullscreen mode Exit fullscreen mode

You should now see output like:

PASS  test/app.test.js
βœ“ should return 200
Enter fullscreen mode Exit fullscreen mode


npm start
Enter fullscreen mode Exit fullscreen mode

Open in browser:
http://localhost:3000

You should see:
Project 1: CI/CD from scratch βœ…

Stop server with:

  • Ctrl + C

🧱 PHASE 2 β€” Dockerize the App (so CI/CD can ship it)
Goal: Package your app into a Docker image that runs the same everywhere.
1) Create Dockerfile (in project root)

Run:

code Dockerfile
Enter fullscreen mode Exit fullscreen mode

This opens up VS Code.
Paste:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

Control + S to save.

2) Create .dockerignore (keeps image clean)

code .dockerignore
Enter fullscreen mode Exit fullscreen mode

Paste:

node_modules
npm-debug.log
.git
.github
Dockerfile
docker-compose.yml
README.md
Enter fullscreen mode Exit fullscreen mode

3) Create docker-compose.yml (local run)

code docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Paste:

services:
  app:
    build: .
    ports:
      - "3000:3000"
Enter fullscreen mode Exit fullscreen mode

Ensure at this stage that your Docker Desktop is powered on.

4) Build + run with Docker Compose
From project root:

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

Test in browser:

You should see:
Project 1: CI/CD from scratch βœ…

Stop:

  • Ctrl + C

Then clean up:

docker compose down
Enter fullscreen mode Exit fullscreen mode

🧱 PHASE 3 β€” Push to GitHub + Add CI (GitHub Actions)
Goal: Every push runs tests automatically. If tests fail β†’ pipeline fails.

1) Commit and push your current code
From project root:

git add .
git commit -m "Phase 2: Add Node app + tests + Docker setup"
git push -u origin main

Enter fullscreen mode Exit fullscreen mode

Got an error.

Git is telling you the truth:

β€œI’m trying to push to a repository that does not exist at that URL.”

So either:

  • the GitHub repo was not created, or
  • the repo name is slightly different from what your local git thinks

We’ll fix this cleanly.

βœ… STEP 1 β€” Check what remote URL Git is using

Run:

git remote -v
Enter fullscreen mode Exit fullscreen mode

From the picture illustration above we can see where the problem is coming from wrong repo (repo not tallying).

Lets reset repo.
Enter the command:

git remote set-url origin https://github.com/dr-musa-bala/start-project1-cicd-from-scratch.git
git remote -v
Enter fullscreen mode Exit fullscreen mode

Now we push again;

git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Check github under same repository.

🧱 PHASE 3 (CONT.) β€” Add GitHub Actions CI
1) Create the workflow file
From your project root:

mkdir -p .github/workflows
code .github/workflows/ci.yml
Enter fullscreen mode Exit fullscreen mode

Paste this:

name: CI

on:
  push:
    branches: ["main"]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test
Enter fullscreen mode Exit fullscreen mode

Save.

2) Commit + push

git add .github/workflows/ci.yml
git commit -m "Add GitHub Actions CI pipeline"
git push
Enter fullscreen mode Exit fullscreen mode

3) Verify it’s running

Go to your GitHub repo β†’ Actions tab β†’ you should see a workflow run.

βœ… If it turns green, you’re done.

βœ… That means Project 1 now has real CI.

Now we level it up into CD: build a Docker image in CI and push it to **Docker Hub **automatically.

🧱 PHASE 4 β€” Docker Hub Build & Push (CD starts)

0) One-time setup on Docker Hub (important)

You need:

Docker Hub username
Docker Hub Access Token (not your password)

Create the token

Docker Hub β†’ Account Settings β†’ Security β†’ New Access Token
Name it: github-actions-project1
Copy it (you won’t see it again).

1) Add GitHub Secrets (this is the β€œsecret stuff” done correctly)

In your GitHub repo:
Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret

Create exactly these:

DOCKERHUB_USERNAME = your Docker Hub username (e.g., drmusabala)

DOCKERHUB_TOKEN = the access token you generated

βœ… Secret names are fixed (left box)
βœ… Secret values are your real details (right box)

2) Update workflow to build & push Docker image

Open the workflow:

code .github/workflows/ci.yml
Enter fullscreen mode Exit fullscreen mode

Replace its content with this (CI + Docker push):

name: CI/CD

on:
  push:
    branches: ["main"]
    tags:
      - "v*.*.*"
  pull_request:

jobs:
  test-build-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "18"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/project1-cicd:latest

Enter fullscreen mode Exit fullscreen mode

3) Commit + push

git add .github/workflows/ci.yml
git commit -m "Add Docker build and push to Docker Hub"
git push
Enter fullscreen mode Exit fullscreen mode

Very critical: Make sure in your secrets, it's your Docker Hub username you are using. Because I kept using my GitHub username and keeps getting errors.

You have successfully built, tested**, **containerized, authenticated, and pushed an image via CI. That’s core DevOps skill.

BONUS:
πŸ“Œ Immediate next steps (important)
1️⃣ Lock this win into documentation (VERY important)

To be pasted in README.md

πŸš€ **Production CI/CD Pipeline β€” Docker & GitHub Actions**

This project demonstrates my ability to design, implement, and troubleshoot a production-grade CI/CD pipeline using modern DevOps tooling.

It reflects how CI/CD is actually done in real teams, including secure credential handling, container image lifecycle management, and failure debugging β€” not just β€œhappy-path” automation.

πŸ” What This Project Proves

- By completing this project end-to-end, I demonstrate the ability to:
- Build automated CI pipelines triggered on source control events
- Containerize applications using Docker best practices
- Securely authenticate and push images to Docker Hub
- Manage secrets and tokens using GitHub Actions
- Debug real CI failures (auth issues, build errors, test failures)
- Think in terms of repeatability, security, and automation
- This is the same workflow used in real production teams β€” simplified, but not diluted.

🧱 Technical Stack

- CI/CD: GitHub Actions
- Containerization: Docker
- Registry: Docker Hub
- Runtime: Node.js
- OS (CI runners): Linux
- Version Control: Git & GitHub
- πŸ” CI/CD Workflow (High-Level)

The pipeline automatically runs on every push to main.

- Pipeline Stages:
- Source checkout
- Node.js environment setup
- Dependency installation
- Test execution
- Secure login to Docker Hub
- Docker image build
- Docker image push to registry

πŸ“ Workflow definition:

.github/workflows/ci.yml

🐳 Docker Image Automation

- The application is built into a Docker image and published automatically.
- Image build is fully automated
- Authentication uses Docker Hub access tokens
- No credentials are committed to source control

Example:

docker pull <dockerhub-username>/start-project1-cicd-from-scratch:latest

πŸ” Security Practices

- Security was treated as a first-class concern, not an afterthought.
- Secrets stored in GitHub Actions Secrets
- Docker Hub authentication via access token, not password
- No sensitive data committed to Git
- Pipeline follows least-privilege principles

Secrets used:

DOCKERHUB_USERNAME

DOCKERHUB_TOKEN

πŸ“‚ Repository Structure
.
β”œβ”€β”€ .github/workflows/ci.yml   # CI pipeline definition
β”œβ”€β”€ Dockerfile                # Production-ready Docker image
β”œβ”€β”€ docker-compose.yml        # Local orchestration
β”œβ”€β”€ index.js                  # Application entry point
β”œβ”€β”€ package.json              # Dependencies & scripts
└── README.md

πŸ§ͺ Testing Philosophy

- Tests are executed automatically during CI
- Pipeline fails fast if tests fail
- Enforces discipline expected in real engineering teams
- This models quality gates commonly used in production pipelines.

🧠 Engineering Mindset Demonstrated

This project emphasizes:

- Automation over manual steps
- Secure-by-default configuration
- Debugging over blind copy-paste
- Reproducibility across environments
- Clear separation between build, test, and release stages

πŸš€ Why This Matters to Employers

This project shows I can:

- Contribute to existing CI/CD systems
- Own small-to-medium automation tasks independently
- Understand how code moves from commit β†’ container β†’ registry
- Communicate infrastructure clearly through code and documentation
- It forms a strong foundation for:
- Continuous Deployment (CD)
- Cloud deployments (EC2, ECS, Kubernetes)
- Infrastructure as Code (Terraform)

πŸ“ˆ Next Steps (Planned Enhancements)

- Automated deployment to cloud (EC2)
- Zero-downtime deployment strategies
- Monitoring & health checks
- Infrastructure provisioning with Terraform

πŸ‘€ About Me

**Dr. Musa Bala Audu**
Junior DevOps Engineer β€” Open to Remote Roles
GitHub: https://github.com/dr-musa-bala
Dev.to: https://dev.to/bala_audu_musa
Hashnode: https://hashnode.com/@musabalaaudu
Enter fullscreen mode Exit fullscreen mode

Your README should include:

  • Project overview
  • CI workflow explanation
  • Docker image link
  • What problem it solves
  • What you learned

βœ… Step 1: Make sure you are in the project directory

In Git Bash, run:

pwd
Enter fullscreen mode Exit fullscreen mode

You should see something like:

/Users/fresh/OneDrive/Desktop/start-project1-cicd-from-scratch
Enter fullscreen mode Exit fullscreen mode

If not, go there:

cd ~/OneDrive/Desktop/start-project1-cicd-from-scratch
Enter fullscreen mode Exit fullscreen mode

βœ… Step 2: Confirm README exists

ls
Enter fullscreen mode Exit fullscreen mode

You must see:

README.md
Enter fullscreen mode Exit fullscreen mode

If you don’t, create it:

touch README.md
Enter fullscreen mode Exit fullscreen mode

βœ… Step 3: Open README and paste the content

Open with VS Code (recommended):

code README.md
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Paste the recruiter-tailored README I wrote for you
πŸ‘‰ Save the file (Ctrl + S)
πŸ‘‰ Close the editor

βœ… Step 4: Stage the README
git add README.md

Check status:

git status

You should see:

new file: README.md

or

modified: README.md

βœ… Step 5: Commit the README (important message)

Use a professional commit message:

git commit -m "docs: add production-grade README for CI/CD project"

βœ… Step 6: Push to GitHub
git push origin main

If main is already tracking:

git push

βœ… Step 7: Verify on GitHub

Open your repo in browser

Refresh the page

You should see:

README rendered automatically

Professional project overview visible to recruiters

Top comments (0)