Building a Full-Stack Cricket with Docker Compose deployed on AWS instance
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/appas our working directoryInstalling 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:
dbservice uses MySQL 5.7 imageappservice builds from our Dockerfiledepends_onensures database starts before the appvolumespersist MySQL data across container restartsServices 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 connectionWe use parameterized queries to prevent SQL injection
Express serves static files from the
publicdirectory
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