From Monolith to Microservices: Building Multi-Container Apps with Docker
Before we get into Docker Compose, we need to set the stage with a real-world problem. Because Docker Compose does not make sense in isolation. It makes sense once you have felt the pain of managing multiple containers by hand. So let us start with an example that most of us interact with every day. A bank. Specifically, we are going to use Bank as our reference architecture for this entire session, and honestly, for the rest of this series all the way through Kubernetes. Pay attention to this example because it is not just a throwaway analogy. We are going to build on it, extend it, and carry it forward. Every concept we introduce from here will connect back to it.

What Does a Real Banking Application Look Like?
Think about everything you can do when you visit a bank's website or open their app.
You have internet banking. You have mobile banking. You have insurance products. You have loan services. And that is just four things out of dozens.
Now here is the question worth asking: do you think all of this runs as one giant application? One codebase, one server, one deployment that does everything?
That approach is called a monolithic architecture. Everything lives together in one big block. And while it sounds simple, it creates serious problems at scale.
What happens when you want to update only the loans module? You have to redeploy the entire application. What happens when the insurance service has a bug and crashes? It can potentially bring down internet banking with it. What happens when you need to scale only mobile banking because there is a surge of users on the app? You end up scaling the whole thing, wasting resources on services that do not need it.
This is exactly why modern applications move toward microservices architecture.
Breaking the Bank into Microservices
In a microservices design, each service lives independently. It has its own codebase, its own deployment, its own lifecycle.
For our example, we are working with four services:
Internet Banking
Mobile Banking
Insurance
Loans
Each of these is a completely separate service. They are not aware of each other's internals. They communicate through well-defined interfaces. And critically for us, each one runs in its own container.
This is the key insight. Separate service means separate container.
So if we need four services, we need four containers. One for internet banking. One for mobile banking. One for insurance. One for loans.
Now, how do we create those containers?
The Workflow Before Docker Compose
Let us think through what we need to do manually to get internet banking running in a container.
To create a container, we need an image. To get an image, we need a Dockerfile. To write a Dockerfile, we need the application code. To get the application code, we go to the repository.
The developer has already written the code and pushed it to GitHub. Our job is to take that code, wrap it in a container, and serve it.
Here is the flow:
GitHub Repository
|
git clone
|
Local EC2 Instance (with code)
|
Write Dockerfile
|
docker build (creates image)
|
docker run (creates container)
|
Application accessible on a port
Let us walk through this step by step.
Step 1: Clone the Repository
The bank project has four separate directories inside the repository, one for each service.
https://github.com/Bhattu-Sai-Praneeth/SHELL-SCRIPTS-REF/tree/main/SAMPLE-WSOnce cloned, you will see a directory structure like this:
hdfc-website/
├── internet-banking/
│ └── index.html
├── mobile-banking/
│ └── index.html
├── insurance/
│ └── index.html
└── loans/
└── index.html
Each directory is a self-contained application. The developer has provided the front-end code as an index.html file for each service.
Step 2: Write a Dockerfile for Internet Banking
Navigate into the internet banking directory:
cd hdfc-website/internet-banking
You will find the index.html file sitting there. That is the application code from the developer.
Now create the Dockerfile:
vi Dockerfile
Here is what the Dockerfile looks like for this service:
FROM ubuntu
RUN apt update -y
RUN apt install apache2 -y
COPY index.html /var/www/html/
EXPOSE 80
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"]
Let us walk through each line.
FROM ubuntu gives us a clean Ubuntu base image to start with.
RUN apt update -y updates the package list so we can install fresh software.
RUN apt install apache2 -y installs the Apache HTTP Server. This is the web server that will serve our HTML files.
A quick note on terminology worth getting right: Apache HTTP Server is a web server, meaning it is designed to serve static files and web content over HTTP. Apache Tomcat, on the other hand, is a Java application server (specifically a servlet container) designed to run Java-based web applications. They are two different tools from the Apache Software Foundation. For our HTML-based banking pages, Apache HTTP Server is exactly what we need.
COPY index.html /var/www/html/ copies the application code from our EC2 instance into the image. The /var/www/html/ directory is where Apache looks for files to serve by default. Whatever you put there becomes publicly accessible.
EXPOSE 80 documents that the application inside the container listens on port 80. Apache always runs on port 80 by default.
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"] starts Apache in foreground mode when a container is created from this image. Running in foreground is critical inside Docker containers. If Apache runs as a background daemon, the process exits immediately, Docker thinks the container has nothing left to do, and the container stops. Running in foreground keeps Apache alive, which keeps the container alive.
Step 3: Build the Image
Now build the Docker image from the Dockerfile:
docker build -t internet-banking:v1 .
Docker reads the Dockerfile, executes each instruction as a separate layer, and produces an image called internet-banking tagged as v1.
Step 4: Create the Container
With the image ready, create the container:
docker run -itd --name internet-banking-container -p 81:80 internet-banking:v1
The -p 81:80 flag is the port mapping. The format is host-port:container-port.
Inside the container, Apache is running on port 80. But we cannot reach the container's internal port 80 directly from a browser. We need to map it to a port on the host EC2 instance. We are choosing port 81 on the host for internet banking.
So when someone opens http://<ec2-ip>:81 in a browser, the request hits the EC2 instance on port 81, gets forwarded to the container on port 80, and Apache serves the internet banking page.
Open the browser, navigate to port 81, and you will see the internet banking landing page. Username field, login button, the full UI. The application is live and running inside a Docker container.
Step 5: Repeat for Every Service
Now comes the repetitive part. We need to do the same thing three more times.
Mobile Banking:
cd ../mobile-banking
vi Dockerfile
The Dockerfile is identical in structure. Same base image, same Apache installation, same copy to /var/www/html/, same CMD.
FROM ubuntu
RUN apt update -y
RUN apt install apache2 -y
COPY index.html /var/www/html/
EXPOSE 80
CMD ["/usr/sbin/apachectl", "-D", "FOREGROUND"]
Build and run it, but this time use port 82 on the host:
docker build -t mobile-banking:v1 .
docker run -itd --name mobile-banking-container -p 82:80 mobile-banking:v1
Mobile banking is accessible at port 82.
Insurance:
Same process, same Dockerfile structure, port 83 on the host.
cd ../insurance
vi Dockerfile
docker build -t insurance:v1 .
docker run -itd --name insurance-container -p 83:80 insurance:v1
Loans:
Same process, port 84 on the host.
cd ../loans
vi Dockerfile
docker build -t loans:v1 .
docker run -itd --name loans-container -p 84:80 loans:v1
After completing all four, you have four running containers on the same EC2 instance. Four separate services. Four separate ports. Each one completely independent.
The Problem This Creates
If you have gone through the exercise of creating all four containers manually, you have already felt the problem.
You had to write four Dockerfiles. You had to run four build commands. You had to run four docker run commands, each with the correct name, port mapping, and image reference. And you had to do it in the right order, without mistakes, every single time.
Now imagine this is not four services. It is twenty services. Or fifty. Real microservices architectures at companies like Netflix or Amazon run hundreds of services. Doing this manually at that scale is not just tedious, it is not operationally viable.
And this is only the creation step. What about starting all the services together after a restart? What about stopping them all cleanly? What about ensuring they all get the right configuration every time? What about when a new developer joins the team and needs to spin up the entire environment on their machine?
All of this falls apart when you are doing it manually.
Enter Docker Compose
The solution to the problem we just described is Docker Compose.
Docker Compose is a tool that allows you to define and run multiple containers using a single YAML configuration file.
Instead of running four separate docker run commands with all their flags and options, you write everything once in a file called docker-compose.yml. Every container, every port mapping, every environment variable, every network connection lives in that one file.
To start all four services, you run one command.
To stop all four services, you run one command.
To rebuild and restart everything from scratch, you run one command.
The docker-compose.yml file becomes the source of truth for your entire multi-container application. It is version-controlled, shareable, and reproducible. Any engineer on your team can clone the repo, run one command, and have the full banking environment running on their machine within minutes.
We are going to write this file in the next session. We will take everything we built manually today, the four Dockerfiles, the four services, the port mappings, and express all of it in a single Docker Compose YAML file. Then we will watch Docker Compose bring everything up in one shot.
Summary
Here is what we covered today and why it matters.
We started with a real-world architecture. Bank runs multiple independent services: internet banking, mobile banking, insurance, and loans. Each service maps directly to its own Docker container. This is the microservices model, and it is the standard architecture for modern production applications.
We then went through the manual process of building and deploying each service. Cloning the code from GitHub, writing a Dockerfile for each service, building images, and running containers with explicit port mappings. Port 81 for internet banking, 82 for mobile banking, 83 for insurance, 84 for loans, with all containers running on Apache with container port 80.
We also established an important distinction. Apache HTTP Server is the web server we are using here, because it excels at serving static web content. Tomcat is a different tool entirely, a Java application server for running Java-based web applications. The right tool for the right job.
Finally, we identified the clear pain point. Doing all of this manually is not scalable. Four services are already tedious. Real applications have dozens or hundreds.
Docker Compose is the answer. One YAML file. One command. All containers up, configured, and connected.
That is exactly where we are going next.






