Step-by-Step Success: Build a 3-Tier Web Application with Docker on AWS

3-Tier Web Application with Docker on AWS EC2 and terraform Overview:

Now we are building a 3-Tier Web Application using Docker on a single EC2 instance (t3.micro) within the AWS Free Tier with Terraform and GitHub. The architecture includes 

  • Frontend – HTML form served via Nginx 
  • Backend – Flask application handling form submissions 
  • Database – MySQL container to store submitted data

3-Tier Web Application project All three containers are connected via the Docker bridge network.

3-Tier Web Application Architecture:

All containers run on the same EC2 instance and communicate using container names through Docker networking.

3-Tier Web Application Technologies Used:

  • Docker for containerization
  • AWS EC2 (t3.micro) for hosting
  • Terraform for infrastructure provisioning
  • Nginx as a static web server
  • Flask as a lightweight backend framework
  • MySQL 5.7 as the relational database

ALSO READ:

Reusable Terraform Modules Made My AWS EC2 Setup Super Easy
Effortless AWS EC2 Deployment with Terraform: Automate Your Infrastructure Today!
Build a 2-Tier Flask MySQL Docker Project

Click here to go GitHub repos link

Terraform—Launch EC2 RHEL 9

Provider.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.91.0"
    }
  }
}

provider "aws" {
  # Configuration options
  region = "us-east-1"
}

Main.tf or ec2.tf

resource "aws_instance" "instance" {
  ami                    = "ami-09c813fb71547fc4f"
  vpc_security_group_ids = [aws_security_group.allow_tl.id]
  instance_type          = "t3.micro"
  
  # 20GB is not enough
  # root_block_device {
  #  volume_size = 50  # Set root volume size to 50GB
  #   volume_type = "gp3"  # Use gp3 for better performance (optional)
  # }

  user_data = file("C:/Devsecops/repos/ec2/installdocker.sh")

  tags = {
    Name    = "docker"
    Purpose = "terraform-sample"
    ENV     = "test"
    project   = "techbasehub"
  }
}

resource "aws_security_group" "allow_tl" {
  name        = "allow_tl"
  description = "Allow TLS inbound traffic and all outbound traffic"
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "allow_tl"
  }
}

Docker installation shell script- installdocker.sh

#!/bin/bash


# growpart /dev/nvme0n1 4
# lvextend -l +50%FREE /dev/RootVG/rootVol
# lvextend -l +50%FREE /dev/RootVG/varVol
# xfs_growfs /
# xfs_growfs /var


sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ec2-user 
# sudo wait $!
echo "User added to docker group"
sudo reboot 

output.tf (optional)

output "public_ip" {
  value = aws_instance.instance.public_ip
}

output "private_ip" {
  value = aws_instance.instance.private_ip
}

output "security_group_name" {
  value = aws_security_group.allow_tl.name
}

output "security_group_id" {
  value = aws_security_group.allow_tl.id
}

output "rander_userdata" {
  value = file("C:/Devsecops/repos/ec2/installdocker.sh")
}
Output – Screenshots

3-Tier Web Application Project Structure:

3-tire-flask-web-mysql-project/
├── frontend/
│   ├── index.html
│   ├── logo_website.jpg
│   └── Dockerfile
├── backend/
│   ├── app.py
│   └── Dockerfile
│   └── requirements.txt
└── db/  # mysql
    └── Dockerfile
    └── init.sql

Frontend (Nginx + HTML Form)

frontend/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Tech Base Hub - Submit Data</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f4f7f8;
      color: #333;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 2rem;
    }
    .container {
      background: white;
      padding: 2rem 3rem;
      border-radius: 8px;
      box-shadow: 0 4px 8px rgba(0,0,0,0.1);
      max-width: 400px;
      width: 100%;
      box-sizing: border-box;
    }
    .logo {
      display: block;
      max-width: 150px;
      margin: 0 auto 1.5rem auto;
    }
    h2 {
      text-align: center;
      margin-bottom: 1.5rem;
      font-weight: 700;
      color: #2c3e50;
    }
    form {
      display: flex;
      flex-direction: column;
    }
    label {
      margin-bottom: 0.5rem;
      font-weight: 600;
    }
    input[type="text"],
    input[type="email"] {
      padding: 0.5rem;
      margin-bottom: 1rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-size: 1rem;
      transition: border-color 0.3s;
    }
    input[type="text"]:focus,
    input[type="email"]:focus {
      border-color: #3498db;
      outline: none;
    }
    input[type="submit"] {
      background-color: #3498db;
      color: white;
      font-weight: 700;
      padding: 0.7rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1.1rem;
      transition: background-color 0.3s;
    }
    input[type="submit"]:hover {
      background-color: #2980b9;
    }
  </style>
</head>
<body>
  <div class="container">
    <img src="logo_website.jpg" alt="Tech Base Hub Logo" class="logo" />
    <h2>Submit Your Details</h2>
    <!-- form action="http://backend:5000/" method="post"  this is real time projects only -->
    <!-- change your bublick IP -->
    <form method="POST" action="http://100.29.17.197:5000/">
      <label for="name">Name:</label>
      <input type="text" id="name" name="name" required />

      <label for="email">Email:</label>
      <input type="email" id="email" name="email" required />

      <input type="submit" value="Submit" />
    </form>
  </div>
</body>
</html>
frontend/logo_website.jpg
frontend/Dockerfile
# Use official nginx image
FROM nginx:alpine

# Remove default nginx website content (optional)
RUN rm -rf /usr/share/nginx/html/*

# Copy your static frontend files to nginx html folder
COPY *.html /usr/share/nginx/html

COPY *.jpg /usr/share/nginx/html

# Expose port 80 (already exposed in base image but good to be explicit)
EXPOSE 80

# Start nginx in foreground
CMD ["nginx", "-g", "daemon off;"]

Backend (Flask)

backend/app.py
from flask import Flask, request, render_template_string
import mysql.connector

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def submit():
    if request.method == 'POST':
        name = request.form.get('name')
        email = request.form.get('email')

        if not name or not email:
            return "Error: Missing name or email field.", 400

        try:
            conn = mysql.connector.connect(
                host='mysql',
                user='root',
                password='root',
                database='appdb'
            )
            cursor = conn.cursor()
            cursor.execute(
                "INSERT INTO submissions (name, email) VALUES (%s, %s)",
                (name, email)
            )
            conn.commit()
            print(f"✅ Successfully inserted: {name}, {email}")
        except Exception as e:
            print(f"❌ Error inserting into database: {e}")
            return "Internal Server Error: Could not save data.", 500
        finally:
            cursor.close()
            conn.close()

        return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Submission Successful</title>
            <style>
                body {
                    font-family: Arial, sans-serif;
                    background-color: #e8f4f8;
                    padding: 40px;
                    text-align: center;
                    color: #333;
                }
                h1 {
                    color: #2e8b57;
                }
                p {
                    font-size: 18px;
                }
                a {
                    text-decoration: none;
                    color: #2e8b57;
                    font-weight: bold;
                }
                a:hover {
                    color: #155d3f;
                }
            </style>
        </head>
        <body>
            <h1>Thank you, {{ name }}!</h1>
            <p>Your data has been submitted successfully.</p>
            <a href="/">Submit another response</a>
        </body>
        </html>
        ''', name=name)
    else:
        return '''
        <!DOCTYPE html>
        <html>
        <head><title>Flask App</title></head>
        <body>
            <h2>Flask backend is up.</h2>
            <p>Use the HTML form from the frontend to submit data.</p>
        </body>
        </html>
        '''

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
backend/Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
backend/requirements.txt
Flask
mysql-connector-python

Database (MySQL)

db/init.sql (Optional)
USE appdb;

CREATE TABLE IF NOT EXISTS submissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

Note:- If you want MySQL to initialize the database and table automatically, use this init.sql and mount it during docker run.

3-Tier Web Application Docker Network & Run Containers

Create a bridge network:

docker network create techbase-network

Output Screenshot

Build and Run MySQL:

docker build -t mysql:1.0.0 .
docker run -d --name mysql --network techbase-network mysql:1.0.0

Build and Run Backend:

docker build -t backend:1.0.0 .
docker run -d --name backend --network techbase-network -p 5000:5000 backend:1.0.0

Build and Run Frontend:

docker build -t frontend:1.0.0 .
docker run -d --name frontend --network techbase-network -p 80:80 frontend:1.0.0

Check the containers

docker ps # docker ps -a
Output:
[ ec2-user@ip-172-31-36-155 ~/dockerfile/3-tire-flask-web-mysql-project/frontend ]$ docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED              STATUS              PORTS                                         NAMES
863dc6e94499   frontend:1.0.0   "/docker-entrypoint.…"   28 seconds ago       Up 28 seconds       0.0.0.0:80->80/tcp, [::]:80->80/tcp           frontend
3379a6268807   backend:1.0.0    "python app.py"          About a minute ago   Up About a minute   0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp   backend
40b7d9a5207e   mysql:1.0.0      "docker-entrypoint.s…"   19 minutes ago       Up 19 minutes       3306/tcp, 33060/tcp                           mysql

3-Tier Web Application Test the Application

Visit: http://public-IP/

http://100.29.17.197 # exaple

Submitt Output

3-Tier Web Application Verify MySQL Data Storage

Connect to the running MySQL container:

docker exec -it mysql bash

Login mysql

mysql -u root -proot

3-Tier Web Application Check the database – appdb

USE appdb;
SELECT * FROM submissions;

3-Tier Web Application Output

[ ec2-user@ip-172-31-36-155 ~/dockerfile/3-tire-flask-web-mysql-project/backend ]$ docker exec -it mysql bash
bash-4.2# mysql -u root -proot
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> USE appdb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT * FROM submissions;
+----+-----------------+--------------------------+
| id | name            | email                    |
+----+-----------------+--------------------------+
|  1 | Tummeti Krishna | krishnatummeti@gmail.com |
|  2 | Krishna         | krishnatummeti@gmail.com |
|  3 | Ravi            | ravi@xyz.com             |
|  4 | Sandeep         | Sandeep@xyz.com          |
+----+-----------------+--------------------------+
4 rows in set (0.00 sec)

mysql>

Thank You

Leave a Comment

Index