Complete Beginner Guide: Node.js + Express API with Docker, Docker Hub, AWS EC2 & CI/CD

WHAT WE ARE BUILDING

A simple Node.js Express REST API with 2 endpoints. One GET endpoint that returns a welcome message. One POST endpoint that receives a JSON payload and sends back a response with the received data. We will containerize it with Docker, push to Docker Hub, host on AWS EC2, and set up CI/CD with GitHub Actions so every code push auto-deploys.


TOOLS NEEDED (Same as before, skip if already installed)

  • VS Code: https://code.visualstudio.com
  • Git: https://git-scm.com/download/win
  • GitHub Desktop: https://desktop.github.com
  • Docker Desktop: https://www.docker.com/products/docker-desktop
  • Node.js: https://nodejs.org → Download LTS version → Install with all defaults

To verify Node.js installed, open VS Code terminal and type:

node --version
npm --version

Both should show version numbers.


ACCOUNTS NEEDED

  • GitHub: https://github.com
  • Docker Hub: https://hub.docker.com
  • AWS: https://aws.amazon.com

PART 1 — CREATE THE PROJECT

Step 1: Create Project Folder

Open File Explorer → go to C:\Users\YourName\ → create a new folder named node-api

Step 2: Open in VS Code

Right-click the node-api folder → "Open with Code"

Step 3: Create the Folder Structure

In VS Code Explorer panel, create this structure:

node-api/
├── src/
│   └── index.js
├── .github/
│   └── workflows/
│       └── cicd.yml
├── Dockerfile
├── .dockerignore
└── package.json

How to create:

  • Click New Folder icon → type "src" → Enter
  • Click New Folder icon → type ".github" → Enter
  • Click on .github → New Folder → type "workflows" → Enter
  • Click on src → New File → type "index.js" → Enter
  • Click on workflows → New File → type "cicd.yml" → Enter
  • Click on root node-api → New File → type "Dockerfile" → Enter
  • New File → type ".dockerignore" → Enter

We will create package.json using a command, not manually.


PART 2 — INITIALIZE NODE PROJECT

Step 4: Open Terminal in VS Code

Press Ctrl + ` to open the terminal at the bottom.

Step 5: Initialize the Project

npm init -y

This creates a package.json file automatically with default values. The -y means yes to all questions. You will see the file appear in your Explorer panel.

Step 6: Install Express

npm install express

This installs Express and creates a node_modules folder and package-lock.json file. Express is the framework we use to create API endpoints easily.

After this your folder looks like:

node-api/
├── node_modules/        ← auto created, do not touch
├── src/
│   └── index.js
├── .github/
│   └── workflows/
│       └── cicd.yml
├── Dockerfile
├── .dockerignore
├── package.json         ← auto created by npm init
└── package-lock.json    ← auto created by npm install

PART 3 — WRITE THE CODE

Step 7: Write index.js (The Main API File)

Click on src/index.js in VS Code and paste this:

const express = require('express');

const app = express();
const PORT = process.env.PORT || 3000;

// This middleware allows Express to read JSON from request body
app.use(express.json());

// ─────────────────────────────────────────
// GET /
// Simple welcome endpoint
// ─────────────────────────────────────────
app.get('/', (req, res) => {
  res.json({
    success: true,
    message: 'Welcome to my Node.js Express API!',
    version: '1.0.0',
    endpoints: {
      GET: 'GET /api/greet?name=YourName',
      POST: 'POST /api/message  →  body: { "name": "...", "text": "..." }'
    }
  });
});

// ─────────────────────────────────────────
// GET /api/greet
// Returns a greeting message
// Optional query param: ?name=YourName
// Example: GET /api/greet?name=Rahul
// ─────────────────────────────────────────
app.get('/api/greet', (req, res) => {
  const name = req.query.name || 'Guest';

  res.json({
    success: true,
    message: `Hello, ${name}! Welcome to the API.`,
    timestamp: new Date().toISOString()
  });
});

// ─────────────────────────────────────────
// POST /api/message
// Receives a JSON payload and sends it back
// Expected body: { "name": "...", "text": "..." }
// Example: POST /api/message
//   body: { "name": "Rahul", "text": "Hello from client" }
// ─────────────────────────────────────────
app.post('/api/message', (req, res) => {
  const { name, text } = req.body;

  // Validate that required fields are present
  if (!name || !text) {
    return res.status(400).json({
      success: false,
      message: 'Both name and text fields are required in the request body.',
      example: {
        name: 'Rahul',
        text: 'Hello from client'
      }
    });
  }

  res.status(201).json({
    success: true,
    message: 'Payload received successfully!',
    received: {
      name: name,
      text: text
    },
    response: `Hi ${name}! We got your message: "${text}"`,
    timestamp: new Date().toISOString()
  });
});

// ─────────────────────────────────────────
// Handle unknown routes
// ─────────────────────────────────────────
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: `Route ${req.method} ${req.url} not found.`
  });
});

// ─────────────────────────────────────────
// Start the server
// ─────────────────────────────────────────
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
  console.log(`Local URL: http://localhost:${PORT}`);
});

Step 8: Update package.json

Open package.json. It was auto-created by npm init. Replace the entire content with this:

{
  "name": "node-api",
  "version": "1.0.0",
  "description": "Simple Node.js Express REST API",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

The start script is important because Docker will use npm start to run the app.

Step 9: Write Dockerfile

Click on Dockerfile and paste this:

# Use official Node.js LTS image based on Alpine (small and fast)
FROM node:20-alpine

# Set working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json first
# We copy these separately so Docker can cache the npm install layer
# This means if only your code changes, npm install won't run again
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy the rest of your application code
COPY src/ ./src/

# Tell Docker this app uses port 3000
EXPOSE 3000

# Command to start the app
CMD ["npm", "start"]

Step 10: Write .dockerignore

Click on .dockerignore and paste this:

node_modules
npm-debug.log
.git
.github
*.md
.env

We ignore node_modules because Docker will install them fresh inside the container using npm install. No need to copy them from your machine.


PART 4 — TEST LOCALLY WITHOUT DOCKER FIRST

Step 11: Run the API on Your Machine

In VS Code terminal:

npm start

You will see:

Server is running on port 3000
Local URL: http://localhost:3000

Step 12: Test the API Endpoints

Open your browser and go to http://localhost:3000. You will see the welcome JSON response.

Now test the GET endpoint. Open browser and go to:

http://localhost:3000/api/greet?name=Rahul

You will see:

{
  "success": true,
  "message": "Hello, Rahul! Welcome to the API.",
  "timestamp": "2026-01-01T10:00:00.000Z"
}

To test the POST endpoint you need a tool because browsers cannot send POST requests directly. Use one of these options:

Option A: Use Thunder Client in VS Code (GUI, Recommended for beginners)

In VS Code, click the Extensions icon (left sidebar, looks like 4 squares). Search "Thunder Client". Install it. Then click the Thunder Client icon in the left sidebar. Click "New Request". Set method to POST. URL: http://localhost:3000/api/message. Click "Body" tab → select "JSON". Paste this in the body:

{
  "name": "Rahul",
  "text": "Hello from Thunder Client"
}

Click Send. You will see the response on the right side.

Option B: Use curl in terminal (CLI)

Open a new terminal (click the + icon in terminal panel) while the server is still running in the first terminal. Run:

curl -X POST http://localhost:3000/api/message -H "Content-Type: application/json" -d "{\"name\": \"Rahul\", \"text\": \"Hello from curl\"}"

You will see:

{
  "success": true,
  "message": "Payload received successfully!",
  "received": {
    "name": "Rahul",
    "text": "Hello from curl"
  },
  "response": "Hi Rahul! We got your message: \"Hello from curl\"",
  "timestamp": "2026-01-01T10:00:00.000Z"
}

Stop the server: press Ctrl+C in the terminal where npm start is running.


PART 5 — TEST WITH DOCKER LOCALLY

Step 13: Build Docker Image

Make sure Docker Desktop is running (whale icon in taskbar). In VS Code terminal:

docker build -t node-api:latest .

You will see Docker going through each step in the Dockerfile. Wait for "Successfully built" message.

GUI verification: Open Docker Desktop → click "Images" → you will see "node-api" listed.

Step 14: Run the Container

docker run -d -p 3000:3000 --name node-api-test node-api:latest

Step 15: Test the Containerized API

Open browser → http://localhost:3000. Same response as before but now running inside Docker.

Test GET: http://localhost:3000/api/greet?name=Docker

Test POST using Thunder Client or curl same as Step 12 but now it is hitting the container.

Step 16: Stop and Remove Test Container

CLI Way:

docker stop node-api-test
docker rm node-api-test

GUI Way: Docker Desktop → Containers → click Stop → click Delete on node-api-test.


PART 6 — DOCKER HUB SETUP

Step 17: Create Docker Hub Repository

  1. Go to https://hub.docker.com and log in
  2. Click "Create Repository"
  3. Name: node-api
  4. Visibility: Public (important — keeps it simple, no login needed to pull)
  5. Click Create

Step 18: Create Docker Hub Access Token

  1. Docker Hub → click your profile photo → Account Settings
  2. Click Security in left menu
  3. Click "New Access Token"
  4. Name: github-actions-token
  5. Permissions: Read, Write, Delete
  6. Click Generate
  7. COPY THE TOKEN and save in Notepad. You will not see it again.

Step 19: Push Image to Docker Hub

In VS Code terminal:

docker login

Enter your Docker Hub username and password.

docker tag node-api:latest yourusername/node-api:latest
docker push yourusername/node-api:latest

Replace "yourusername" with your actual Docker Hub username. For example if username is gaggarwal124:

docker tag node-api:latest gaggarwal124/node-api:latest
docker push gaggarwal124/node-api:latest

Go to https://hub.docker.com/r/yourusername/node-api and verify the image is there with "latest" tag.


PART 7 — PUSH TO GITHUB

Step 20: Create GitHub Repository

GUI Way using GitHub Desktop:

  1. Open GitHub Desktop
  2. Click "Add an Existing Repository from your Hard Drive"
  3. Browse to your node-api folder → Select Folder
  4. It will say "not a git repository" → click "create a repository" link
  5. Name: node-api
  6. Click "Create Repository"
  7. Click "Publish repository" at the top
  8. Uncheck "Keep this code private" if you want public
  9. Click "Publish Repository"

CLI Way:

git init
git add .
git commit -m "Initial commit: Node.js Express API"

Go to https://github.com → click + → New repository → name it "node-api" → Create repository. Then copy the commands GitHub shows you and run them. They look like:

git remote add origin https://github.com/yourusername/node-api.git
git branch -M main
git push -u origin main

PART 8 — AWS EC2 SETUP

If you already have an EC2 instance from the previous project with Docker installed, you can reuse it. Just make sure port 3000 is open in the security group. Skip to Step 22 if reusing.

Step 21: Create New EC2 Instance (Skip if reusing old one)

  1. Go to https://console.aws.amazon.com

  2. Search "EC2" in top search bar → click EC2

  3. Click orange "Launch Instance" button

  4. Fill in settings:

    Name: node-api-server

    AMI: Amazon Linux 2023 (Free tier eligible)

    Instance type: t2.micro (Free tier eligible)

    Key pair: Click "Create new key pair"

    • Name: node-api-key
    • Type: RSA
    • Format: .pem
    • Click Create key pair
    • Save the downloaded .pem file to C:\Users\YourName.ssh\

    Network settings → click Edit:

    • Rule 1: SSH, port 22, Source: My IP
    • Rule 2: HTTP, port 80, Source: Anywhere 0.0.0.0/0
    • Click "Add security group rule":
    • Rule 3: Custom TCP, port 3000, Source: Anywhere 0.0.0.0/0 (this is for our Node API)
  5. Click "Launch Instance"

  6. Click "View all instances"

  7. Wait for status to show "Running" with green dot

  8. Note the Public IPv4 address (example: 13.235.xxx.xxx)

Step 22: Install Docker on EC2

GUI Way: In AWS Console → EC2 → Instances → select your instance → click "Connect" → "EC2 Instance Connect" tab → click "Connect". Browser terminal opens.

CLI Way: Open VS Code terminal and run:

ssh -i C:\Users\YourName\.ssh\node-api-key.pem ec2-user@YOUR_EC2_PUBLIC_IP

Now run these commands in the EC2 terminal:

sudo yum update -y
sudo yum install docker -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ec2-user

Close the terminal and reconnect. Then verify:

docker --version

PART 9 — ADD GITHUB SECRETS

Step 23: Add Secrets to GitHub Repository

Go to https://github.com/yourusername/node-api → Settings tab → Secrets and variables → Actions → New repository secret.

Add these 5 secrets:

Secret 1:

  • Name: DOCKERHUB_USERNAME
  • Value: your Docker Hub username (example: gaggarwal124)

Secret 2:

  • Name: DOCKERHUB_TOKEN
  • Value: the token you saved in Notepad from Step 18

Secret 3:

  • Name: EC2_HOST
  • Value: your EC2 public IP address (example: 13.235.243.85)

Secret 4:

  • Name: EC2_USERNAME
  • Value: ec2-user

Secret 5:

  • Name: EC2_SSH_KEY
  • Value: open your .pem file in VS Code, select all (Ctrl+A), copy, paste here. Make sure the entire content is there including the first line "-----BEGIN RSA PRIVATE KEY-----" and last line "-----END RSA PRIVATE KEY-----"

PART 10 — CREATE CI/CD PIPELINE

Step 24: Write cicd.yml

Click on .github/workflows/cicd.yml in VS Code and paste this complete final file:

name: CI/CD Pipeline - Node API

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

jobs:

  # ================================
  # JOB 1: Build and Push to Docker Hub
  # ================================
  build-and-push:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest

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

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

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

      - name: Build and Push image to Docker Hub
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/node-api:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/node-api:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ================================
  # JOB 2: Deploy to AWS EC2
  # ================================
  deploy:
    name: Deploy to AWS EC2
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: SSH into EC2 and Deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            # Login to Docker Hub (handles private repos too)
            echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin

            # Pull the latest image
            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/node-api:latest

            # Stop and remove old container if it exists
            docker stop node-api || true
            docker rm node-api || true

            # Run new container
            docker run -d \
              --name node-api \
              --restart unless-stopped \
              -p 3000:3000 \
              ${{ secrets.DOCKERHUB_USERNAME }}/node-api:latest

            # Clean up old unused images to save disk space
            docker image prune -f

            echo "Deployment successful! API is live."

PART 11 — DEPLOY

Step 25: Push Everything to GitHub

GUI Way using GitHub Desktop:

  1. Open GitHub Desktop
  2. You will see all your files listed on the left as changes
  3. In the Summary field at bottom left type: Add Node.js Express API with Docker and CI/CD
  4. Click "Commit to main"
  5. Click "Push origin" at the top

CLI Way:

git add .
git commit -m "Add Node.js Express API with Docker and CI/CD"
git push origin main

Step 26: Watch the Pipeline Run

  1. Go to https://github.com/yourusername/node-api
  2. Click the "Actions" tab
  3. You will see a workflow running with a yellow spinner
  4. Click on it to open
  5. Watch Job 1 "Build and Push Docker Image" run — takes about 2-3 minutes
  6. Watch Job 2 "Deploy to AWS EC2" run — takes about 1 minute
  7. Both should show green checkmarks when done

If any job fails, click on it to see the error log and share a screenshot.

Step 27: Test Your Live API on AWS

Open your browser and go to:

http://YOUR_EC2_PUBLIC_IP:3000

Example: http://13.235.243.85:3000

You will see the welcome JSON. Now test the GET endpoint:

http://YOUR_EC2_PUBLIC_IP:3000/api/greet?name=Rahul

To test the POST endpoint, use Thunder Client in VS Code. Create a new POST request. URL: http://YOUR_EC2_PUBLIC_IP:3000/api/message. Body JSON:

{
  "name": "Rahul",
  "text": "Hello from my deployed API!"
}

Send. You will get:

{
  "success": true,
  "message": "Payload received successfully!",
  "received": {
    "name": "Rahul",
    "text": "Hello from my deployed API!"
  },
  "response": "Hi Rahul! We got your message: \"Hello from my deployed API!\"",
  "timestamp": "2026-01-01T10:00:00.000Z"
}

COMPLETE FOLDER STRUCTURE (Final)

node-api/
├── .github/
│   └── workflows/
│       └── cicd.yml          ← GitHub Actions pipeline
├── src/
│   └── index.js              ← Express API with GET and POST endpoints
├── node_modules/             ← Auto created by npm install, do not touch
├── .dockerignore             ← Files to ignore when building Docker image
├── Dockerfile                ← Instructions to build Docker image
├── package.json              ← Project info and dependencies
└── package-lock.json         ← Auto created by npm install

API ENDPOINTS SUMMARY

GET  /                        → Welcome message and list of endpoints
GET  /api/greet               → Returns greeting (optional ?name=YourName)
POST /api/message             → Receives JSON payload, returns it back

COMPLETE FLOW

You write code in VS Code
         ↓
Push to GitHub via GitHub Desktop or git push
         ↓
GitHub Actions triggers automatically
         ↓
Job 1: Builds Docker image → Pushes to Docker Hub
         ↓
Job 2: SSH into EC2 → docker login → docker pull → docker run
         ↓
API is live at http://YOUR_EC2_IP:3000

HOW TO UPDATE THE API EVERY TIME

Make any change in src/index.js. For example add a new endpoint or change a message. Then:

GitHub Desktop: write commit message → Commit to main → Push origin.

CLI: git add . → git commit -m "your message" → git push origin main.

GitHub Actions runs automatically. In 3-4 minutes your changes are live on AWS without touching the server manually.

No comments:

Post a Comment

Complete Beginner Guide: Node.js + Express API with Docker, Docker Hub, AWS EC2 & CI/CD

WHAT WE ARE BUILDING A simple Node.js Express REST API with 2 endpoints. One GET endpoint that returns a welcome message. One POST endpoint ...