📍Introduction
Welcome to my Docker blog series! Here, we'll explore docker-compose and do a hand's-on project by containerizing and deploying a 2-tier flask to-do web application and also troubleshooting the errors we will be facing.
Prerequisite: Make sure you know Docker Fundamentals if not or if you need a brush up you can read my previous blogs:
📍Docker Compose
Up until now, we've explored how to containerize individual services or simple applications. However, real-world applications often follow a multi-tier architecture involving 2 or 3 tiers.
Consider a scenario where we aim to containerize a 3-tier architecture application, comprising:
Front-end
Back-end
Database
To containerize this application, we adopt a strategy of isolating each program into distinct containers to containerize this intricate setup. Thus, we establish three separate containers one for the front end, another for the back end, and a third for the database. We accomplish this by crafting a Dockerfile for each component.
The approach involves two main steps:
Organizing Directory Structure: We create two directories—one for the front end and another for the back end. Within these directories, we place the respective codes along with their dedicated Dockerfiles. Meanwhile, the Dockerfile for the database resides in the project directory where the front-end and back-end directories are located.
Building and Running Containers: Once the directories and Dockerfiles are in place, we initiate the process by executing
docker build
anddocker run
for eachDockerfile
. This step efficiently constructs the containers, encapsulating the distinct tiers.
Consider this scenario: We previously discussed a 3-tier architecture, but now imagine a more complex microservice architecture where each service needs to be containerized. The manual approach we discussed earlier would become impractical in this context. Fortunately, there's a more efficient solution: "Docker Compose."
Docker Compose is a tool that comes with a program called docker-compose. It offers a streamlined method for managing multi-container applications. Instead of individually building and launching containers, Docker Compose allows us to define all the necessary actions in a single configuration file. This file outlines how to build and run multiple Docker containers concurrently, eliminating the need for repetitive tasks. Operations like docker-compose up
and docker-compose down
simplify the process further. The configuration file itself is written in YAML (Yet Another Markup Language), and it typically carries the name "docker-compose.yml
" for convention's sake
Some common Docker Compose commands:
docker-compose up
: Build and start containers as defined in thedocker-compose.yml
file.docker-compose up -d
: Build and start containers in the background (detached mode).docker-compose down
: Stop and remove containers, networks, and volumes defined in thedocker-compose.yml
file.docker-compose build
: Build images for services defined in thedocker-compose.yml
file.docker-compose start
: Start containers that are already defined in thedocker-compose.yml
file.docker-compose stop
: Stop containers that are already defined in thedocker-compose.yml
file.docker-compose ps
: List running containers defined in thedocker-compose.yml
file.docker-compose logs
: Display logs from containers defined in thedocker-compose.yml
file.docker-compose exec
: Execute a command in a running container.docker-compose run
: Run a one-off command in a new container.docker-compose pull
: Pull images from the registry as defined in thedocker-compose.yml
file.docker-compose images
: List images used by the services in thedocker-compose.yml
file.docker-compose config
: Validate and view the final configuration after variable substitution in thedocker-compose.yml
file
📝Make sure to run these commands where the docker-compose.yml
file is present.🖊
📍YAML: Configuration Language
YAML is a markup language and is commonly used alongside JSON for creating configuration files. Let's explore how to compose a YAML file.
To understand the syntax of YAML we will be taking an example of a Python data structure "dictionary".
dict= ["name": "Varun", "age": 21, "hobbies": {singing, dancing, guitar}]
# Check out my Python blog if you don't know about Python dictionary.
Python: Empowering DevOps with Automation and Efficiency (Part 2)
We will be converting this Python dictionary of key-value pairs into YAML
name: Varun
age: 21
hobbies: # It contains a list in YAML it is written as this:
- singing
- dancing
- guitar
# This indentation (spacing) is very important as we do not have {} brackets to denote where does this key-value pair belong
# We can put numbers as strings using "" example
password: "12345"
That's it, it is this simple to write a YAML file. You can see it is similar to Python Dictionary with key-value pair but does not include {}
brackets.
📍Docker Compose File Structure
Configuration File name: docker-compose.yml
should always be named this by convention otherwise docker-compose
commands will not recognize the file.
version: '3' # Specify the version of Docker Compose syntax
services:
service_name1: # Name of the first service (example: frontend)
image: image_name1:tag # Docker image for the service
ports:
- "host_port:container_port" # Map host port to container port
environment:
ENV_VARIABLE1: value1 # Environment variables for the service
service_name2: # Name of the second service (backend or database)
image: image_name2:tag
volumes:
- volume_name:container_path # Mount a volume to the container
depends_on:
- service_name1 # Depend on another service
networks:
network_name: # Define custom networks if needed
driver: bridge
volumes:
volume_name: # Define named volumes for data persistence
version
: Specifies the version of the Docker Compose syntax being used.services
: Defines the various services (containers) that make up your application. Each service has its configuration options, including the Docker image to use, ports to expose, environment variables, and more.image
: Specifies the Docker image to use for the service, along with an optional tag.ports
: Maps host ports to container ports, allowing you to access the service from outside the container.environment
: Sets environment variables for the service.volumes
: Defines volumes that can be mounted to containers, allowing data persistence.depends_on
: Specifies the order in which services are started. Services listed here will start before the service using this option.networks
: Defines custom networks if needed. Networks allow communication between containers.volumes
: Defines named volumes that can be used for data persistence across containers.
We will discuss docker volumes and docker networks in the next upcoming blog.
📝Remember that the structure can be more complex based on the needs of your application. You can have multiple services, networks, and volumes defined in a single Docker Compose file. The structure helps orchestrate the deployment and management of your multi-container application.
Now that we know how to write a docker-compose.yml file. Let's do a hand's-on project by containerizing and deploying a 2-tier flask to-do web application using docker-compose this will make things more clear.🚀
📍2-Tier Flask Web Application
We will be containerizing a two-tier Flask web application, it is a simple to-do app. We will make 2 containers:
Container 1: Contains the front end and the backend
Container 2: Contains the database that will be running — MySQL, in our case.
Here is the File Structure for the project:
📁two-tier-flask-app (Project folder/directory name)
📄app.py (backend)
🐳Dockerfile
🐳docker-compose.yml
📄requirements.txt (Contains all the dependencies)
📁templates (directory for the front end)
- 📄index.html (Basic frontend)
This will be our project file structure. Copy the code into their respective file.
Step 1
: Create a file app.py
and copy this code in it:
import os
from flask import Flask, render_template, request, redirect, url_for
from flask_mysqldb import MySQL
app = Flask(__name__)
# Configure MySQL from environment variables
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB')
# Initialize MySQL
mysql = MySQL(app)
@app.route('/')
def index():
cur = mysql.connection.cursor()
cur.execute('SELECT * FROM tasks')
tasks = cur.fetchall()
cur.close()
return render_template('index.html', tasks=tasks)
@app.route('/add', methods=['POST'])
def add():
new_task = request.form.get('new_task')
cur = mysql.connection.cursor()
cur.execute('INSERT INTO tasks (task) VALUES (%s)', [new_task])
mysql.connection.commit()
cur.close()
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Step 2
: Create a file requirements.txt
Flask==2.0.1
Flask-MySQLdb==0.2.0
mysqlclient==2.1.0
Step 3
: Create a Dockerfile for the backend app.py
# Use an official Python runtime as the base image
FROM python:3.9-slim
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y gcc default-libmysqlclient-dev pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory in the container
WORKDIR /app
# Copy all the files into the container
COPY . .
# Install app dependencies
RUN pip install --upgrade pip \
&& pip install mysqlclient \
&& pip install -r requirements.txt
# Specify the command to run your application
CMD ["python", "app.py"]
📍Docker Compose for a 2-Tier application
Step 4
: Create docker-compose.yml
file
version: '3'
services:
backend:
build:
context: .
ports:
- "5000:5000"
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: root_password # Make sure to add a root password
MYSQL_DB: todo_db # Create a new database for the to-do app
depends_on:
- mysql
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root_password # Make sure to add your root password
MYSQL_DATABASE: todo_db # Create a new database for the to-do app
MYSQL_USER: user_name # Make sure to add your username, password
MYSQL_PASSWORD: user_password
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:
We will be seeing about Docker volumes in my next blog. For now, just understand that docker volumes are created so that the container data can be persisted in our system if the container gets crashed.
Step 5
: Create a templates directory and inside it create an index.html
file
<!DOCTYPE html>
<html>
<head>
<title>To-Do List App</title>
</head>
<body>
<h1>To-Do List</h1>
<ul>
{% for task in tasks %}
<li>{{ task[1] }}</li>
{% endfor %}
</ul>
<form action="/add" method="post">
<input type="text" name="new_task" placeholder="Add a new task">
<input type="submit" value="Add">
</form>
</body>
</html>
📍Deploying a Two-Tier Flask app using docker-compose
Now that our project file structure is ready, let's containerize and deploy the Flask app:
Run: docker-compose up -d
When you see this output:
This means your container is up and running to see our deployed Flask application:
Go to Browser and search: localhost:5000
In my case I am using an AWS EC2 instance, therefore for me ec2_ip_address:5000
You will get this error saying the database 'todo_db' does not exist.
For some reason, it did not create a MySQL database, to avoid this we can do:
Create a todo_db database manually while the MySQL container is running.
Step 1
: Press the Ctrl + c
key to stop the program.
Step 2
: docker-compose up -d
This will run the program in a detached mode in the background this will help us give the command terminal:
You can see that the containers are running. Let's get inside the MySQL container using the command:
docker exec -it <mysql_container_id> /bin/bash
: This will give us a bash shell to interact with the MySQL container.
Login to the MySQL container from the bash using mysql -u root -p
Enter the root password in our case it is root@123
Let's see the list of databases:
As we can see there is no todo_db database created. Let's create:
MySQL query to create the database: create database todo_db;
Now that the database is created let's refresh and see our deployed application:
Now it is giving an error called "Table 'todo_tasks' does not exist"
Let's create this table:
MySQL query to create a table: create table todo_db.tasks (task varchar(255))
(If you don't know this just do a simple Google search. It is ok we learn as we practice and build more projects.)
Let's refresh and see our Flask application:
Yay!!!🤩 Our 2-tier Flask To-do web application has been successfully deployed.
🎉Congratulations you have containerized a 2-tier we application using docker-compose. You can add this project to your resume!🥳
📝Note: Initially, some might find this process challenging. If you encounter errors, don't be discouraged; simply search for them online or consult resources like ChatGPT to troubleshoot effectively.
Even I faced challenges with my first 2-tier project using docker-compose and struggled with errors for days. Persevere and continue experimenting eventually, you'll achieve a successful deployment.🖊
📍Conclusion
Now that you've successfully deployed a two-tier application as part of your practice, you can further enhance your skills by selecting any existing 2-tier or 3-tier application. Transform it into a containerized setup by crafting Dockerfiles and configuring docker-compose files, all while mastering the art of troubleshooting. This will enable you to enrich your resume/portfolio with diverse projects. This approach sharpens your expertise in Docker and equips you to adeptly containerize intricate microservices architectures.
Thank you for reading this blog! 📖 Hope you have gained some value.
If you enjoyed this blog and found it helpful, please give it a like 👍, share it with your friends, do share your thoughts, and give me some valuable feedback.😇 Don't forget to follow me for more such blogs! 🌟