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