Skip to main content

Command Palette

Search for a command to run...

Building a Full-Stack Cricket with Docker Compose deployed on AWS instance

Published
9 min read

Introduction

Today, I'm excited to share how I built a complete full-stack application from scratch using Docker, Node.js, Express, and MySQL. This project is perfect for anyone looking to understand Docker Compose, multi-container applications, and how to orchestrate microservices.

We'll be building a Cricket Player Management System - a web application where users can submit and store information about cricket players. But the real star of the show here is Docker Compose and how it simplifies our development workflow.

What You'll Learn

By the end of this tutorial, you'll understand:

  • 🐳 How to containerize a Node.js application

  • 🔧 Setting up multi-container applications with Docker Compose

  • 💾 Managing persistent data with Docker volumes

  • 🌐 Connecting Node.js with MySQL in containers

Architecture Overview

Our application consists of two main services:

┌─────────────────────────────────────┐
│         User's Browser              │
│      (http://localhost:3000)        │
└──────────────┬──────────────────────┘
               │
               │ HTTP Requests
               ▼
┌─────────────────────────────────────┐
│       Node.js Application           │
│    (Express.js + Body Parser)       │
│         Running on Port 3000        │
└──────────────┬──────────────────────┘
               │
               │ MySQL Protocol
               ▼
┌─────────────────────────────────────┐
│         MySQL Database              │
│         (Version 5.7)               │
│       Database: cricket_db          │
└─────────────────────────────────────┘

Step 1: Setting Up Docker and Docker Compose

First, let's get our environment ready.

Launch an EC2 Instance

  • Choose Amazon Linux 2 or Ubuntu 22.04 LTS

  • Instance Type: t2.micro (Free Tier)

  • Open Port 3000 (for app) and 3306 (for MySQL, optional) in Security Groups

SSH into your instance:

ssh -i "your-key.pem" ec2-user@<EC2-Public-IP>

Install Docker

For Amazon Linux/CentOS/RHEL:

sudo yum update -y
sudo yum install docker -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER

For Ubuntu/Debian:

sudo apt update
sudo apt install docker.io -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER

Install Docker Compose

# Download Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/v2.10.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# Make it executable
sudo chmod +x /usr/local/bin/docker-compose

# Verify installation
docker-compose --version

Step 2: Project Structure

Let's create our project directory structure:

mkdir cricket-app && cd cricket-app
mkdir public

Your final structure should look like this:

cricket-app/
├── Dockerfile
├── docker-compose.yml
├── package.json
├── app.js
└── public/
    └── index.html

Step 3: Creating the Dockerfile

The Dockerfile defines how our Node.js application will be containerized.

Create Dockerfile:

# Use official Node.js 14 image
FROM node:14

# Set working directory inside container
WORKDIR /usr/src/app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application source code
COPY . .

# Expose port 3000
EXPOSE 3000

# Start the application
CMD ["node", "app.js"]

What's happening here?

  • We're using Node.js 14 as our base image

  • Setting /usr/src/app as our working directory

  • Installing npm dependencies

  • Copying our application code

  • Exposing port 3000 for external access

Step 4: Docker Compose Configuration

Docker Compose allows us to define and run multi-container applications. Create docker-compose.yml:

version: '3.8'

services:
  # Database Service
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: cricket_db
    volumes:
      - db_data:/var/lib/mysql

  # Application Service
  app:
    build: .
    restart: always
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      DB_HOST: db
      DB_USER: root
      DB_PASS: root

volumes:
  db_data:

Key points:

  • db service uses MySQL 5.7 image

  • app service builds from our Dockerfile

  • depends_on ensures database starts before the app

  • volumes persist MySQL data across container restarts

  • Services communicate using Docker's internal network

Step 5: Node.js Application

Create package.json:

{
  "name": "cricket-app",
  "version": "1.0.0",
  "main": "app.js",
  "dependencies": {
    "express": "^4.17.1",
    "mysql": "^2.18.1",
    "body-parser": "^1.19.0"
  }
}

Create app.js:

const express = require('express');
const bodyParser = require('body-parser');
const mysql = require('mysql');

const app = express();
const port = 3000;

// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// MySQL Connection
const db = mysql.createConnection({
    host: 'db',  // Docker service name
    user: 'root',
    password: 'root',
    database: 'cricket_db'
});

// Connect to MySQL
db.connect(err => {
    if (err) {
        console.error('Database connection failed:', err);
        return;
    }
    console.log('Connected to MySQL database');
});

// Serve static files
app.use(express.static('public'));

// API endpoint to submit data
app.post('/submit', (req, res) => {
    const { cricketerName, countryName } = req.body;

    const sql = 'INSERT INTO cricketers (name, country) VALUES (?, ?)';
    db.query(sql, [cricketerName, countryName], (err, result) => {
        if (err) {
            console.error('Error inserting data:', err);
            res.status(500).send('Error inserting data');
            return;
        }
        res.send('Congratulations, you have successfully deployed');
    });
});

// Start server
app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

Important notes:

  • The host: 'db' uses Docker's service name for connection

  • We use parameterized queries to prevent SQL injection

  • Express serves static files from the public directory

Step 6: Frontend Interface

Create public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cricket Legends Hub ✨</title>
    <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Inter', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
            position: relative;
            overflow-x: hidden;
        }

        body::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: url('data:image/svg+xml,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="2" fill="rgba(255,255,255,0.1)"/></svg>');
            animation: float 20s linear infinite;
        }

        @keyframes float {
            from { transform: translateY(0); }
            to { transform: translateY(-100px); }
        }

        .container {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(20px);
            border-radius: 24px;
            padding: 40px;
            max-width: 550px;
            width: 100%;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            position: relative;
            z-index: 1;
            animation: slideUp 0.6s ease-out;
        }

        @keyframes slideUp {
            from {
                opacity: 0;
                transform: translateY(30px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .header {
            text-align: center;
            margin-bottom: 30px;
        }

        .emoji-header {
            font-size: 48px;
            margin-bottom: 10px;
            animation: bounce 2s ease-in-out infinite;
        }

        @keyframes bounce {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(-10px); }
        }

        h1 {
            font-family: 'Space Grotesk', sans-serif;
            font-size: 32px;
            font-weight: 700;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin-bottom: 8px;
        }

        .subtitle {
            color: #666;
            font-size: 14px;
            font-weight: 500;
        }

        form {
            margin: 30px 0;
        }

        .input-group {
            margin-bottom: 20px;
            position: relative;
        }

        label {
            display: block;
            margin-bottom: 8px;
            color: #333;
            font-weight: 600;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        input {
            width: 100%;
            padding: 16px 20px;
            border: 2px solid #e0e0e0;
            border-radius: 12px;
            font-size: 16px;
            font-family: 'Inter', sans-serif;
            transition: all 0.3s ease;
            background: #f9f9f9;
        }

        input:focus {
            outline: none;
            border-color: #667eea;
            background: white;
            box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
            transform: translateY(-2px);
        }

        .submit-btn {
            width: 100%;
            padding: 18px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            text-transform: uppercase;
            letter-spacing: 1px;
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
        }

        .submit-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
        }

        .submit-btn:active {
            transform: translateY(0);
        }

        #responseMessage {
            margin-top: 20px;
            padding: 16px;
            border-radius: 12px;
            font-weight: 500;
            text-align: center;
            animation: fadeIn 0.5s ease;
            display: none;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(-10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        #responseMessage.show {
            display: block;
        }

        #responseMessage.success {
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            color: white;
        }

        #responseMessage.error {
            background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
            color: white;
        }

        .social-links {
            display: flex;
            gap: 12px;
            margin-top: 30px;
            padding-top: 30px;
            border-top: 2px solid #f0f0f0;
        }

        .social-btn {
            flex: 1;
            padding: 14px;
            border-radius: 12px;
            border: none;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            text-decoration: none;
            color: white;
        }

        .youtube-btn {
            background: #FF0000;
        }

        .youtube-btn:hover {
            background: #cc0000;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(255, 0, 0, 0.4);
        }

        .linkedin-btn {
            background: #0077B5;
        }

        .linkedin-btn:hover {
            background: #005582;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0, 119, 181, 0.4);
        }

        .stats {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }

        .stat-card {
            flex: 1;
            padding: 16px;
            background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
            border-radius: 12px;
            text-align: center;
        }

        .stat-number {
            font-size: 24px;
            font-weight: 700;
            color: #667eea;
            font-family: 'Space Grotesk', sans-serif;
        }

        .stat-label {
            font-size: 12px;
            color: #666;
            margin-top: 4px;
            font-weight: 500;
        }

        @media (max-width: 600px) {
            .container {
                padding: 30px 20px;
            }

            h1 {
                font-size: 28px;
            }

            .social-links {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <div class="emoji-header">🏏</div>
            <h1>Cricket Legends Hub</h1>
            <p class="subtitle">Add your favorite cricket stars to the hall of fame</p>
        </div>

        <div class="stats">
            <div class="stat-card">
                <div class="stat-number" id="totalEntries">0</div>
                <div class="stat-label">LEGENDS ADDED</div>
            </div>
            <div class="stat-card">
                <div class="stat-number" id="totalCountries">0</div>
                <div class="stat-label">COUNTRIES</div>
            </div>
        </div>

        <form id="cricketerForm">
            <div class="input-group">
                <label for="cricketerName">🌟 Cricketer Name</label>
                <input type="text" id="cricketerName" name="cricketerName" placeholder="e.g., Virat Kohli" required>
            </div>

            <div class="input-group">
                <label for="countryName">🌍 Country</label>
                <input type="text" id="countryName" name="countryName" placeholder="e.g., India" required>
            </div>

            <button type="submit" class="submit-btn">Add Legend ✨</button>
        </form>

        <div id="responseMessage"></div>


    </div>

    <script>
        let totalEntries = 0;
        const countries = new Set();

        document.getElementById('cricketerForm').addEventListener('submit', function(e) {
            e.preventDefault();
            const cricketerName = document.getElementById('cricketerName').value;
            const countryName = document.getElementById('countryName').value;
            const responseMsg = document.getElementById('responseMessage');

            fetch('/submit', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ cricketerName, countryName }),
            })
            .then(response => response.text())
            .then(data => {
                responseMsg.className = 'show success';
                responseMsg.innerHTML = `🎉 ${cricketerName} from ${countryName} added successfully!`;

                // Update stats
                totalEntries++;
                countries.add(countryName);
                document.getElementById('totalEntries').textContent = totalEntries;
                document.getElementById('totalCountries').textContent = countries.size;

                // Clear form
                document.getElementById('cricketerForm').reset();

                // Hide message after 3 seconds
                setTimeout(() => {
                    responseMsg.className = '';
                }, 3000);
            })
            .catch((error) => {
                console.error('Error:', error);
                responseMsg.className = 'show error';
                responseMsg.innerHTML = '❌ Oops! Something went wrong. Try again!';

                setTimeout(() => {
                    responseMsg.className = '';
                }, 3000);
            });
        });
    </script>
</body>
</html>

Step 7: Launch the Application

Now comes the magic moment! 🎉

# Build and start containers
docker-compose up --build -d

# Check if containers are running
docker ps

You should see both containers running:

  • cricket-app-app-1 (Node.js application)

  • cricket-app-db-1 (MySQL database)

Step 8: Initialize the Database

We need to create the database table:

# Access MySQL container
docker exec -it cricket-app-db-1 mysql -u root -p
# Password: kastro

Inside MySQL shell, run:

-- Show databases
SHOW DATABASES;

-- Use our database
USE cricket_db;

-- Create table
CREATE TABLE cricketers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    country VARCHAR(255) NOT NULL
);

-- Verify table
SHOW TABLES;
DESCRIBE cricketers;

-- Exit
EXIT;

Step 9: Test the Application

Open your browser and navigate to:

http://localhost:3000

Try submitting some data:

  • Cricketer Name: Virat Kohli

  • Country: India

Step 10: Verify Data Storage

Let's check if our data is actually stored:

# Access MySQL again
docker exec -it cricket-app-db-1 mysql -u root -p

# Query the data
USE cricket_db;
SELECT * FROM cricketers;

You should see your submitted data! 🎊

Useful Commands

Here are some handy commands for managing your application:

# View logs
docker-compose logs -f

# Stop application
docker-compose down

# Restart application
docker-compose restart

# View database logs only
docker-compose logs db

# Rebuild and restart
docker-compose up --build -d

#Docker #NodeJS #MySQL #DevOps #WebDevelopment #FullStack #Tutorial

J

Hey Akansha, I’ve been following your posts I’m currently learning DevOps and really admire how clearly you explain complex stuff.

I’m trying to get better at devops and I’d love any advice or guidance you could share on how to approach it or what projects to start with.

Totally understand if you’re busy, but even a quick tip or direction would mean a lot. Thanks!