Skip to main content

Command Palette

Search for a command to run...

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.

Updated
10 min read
From Monolith to Microservices: Building Multi-Container Apps with Docker
S
Passionate about Cloud and DevOps engineering. I write structured technical notes and beginner-friendly articles on AWS, Linux, CI/CD, networking, system architecture and modern software delivery workflows.

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.

More from this blog

Ansible Playbooks Explained: From First YAML File to Managing Real Servers

If you have already run a few Ansible ad hoc commands and seen how they work, you already understand the core idea: one command, many servers. But ad hoc commands only take you so far. When you need to install software, start services, create users, copy files, and print confirmation messages, all in one automated run across multiple servers, that is when you move to playbooks. Playbooks are where Ansible truly earns its place in a DevOps workflow. Everything you do in Ansible at scale, you do through playbooks.

Jun 19, 202613 min read
Ansible Playbooks Explained: From First YAML File to Managing Real Servers

You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Picture this. You have four EC2 instances running in your AWS account, and someone asks you to install Apache on all four of them. What do you do? The obvious answer most people go with is SSH into each machine, run the install command, repeat. Simple enough when it is four servers. But what happens when it is forty? Or four hundred? In a real enterprise environment, that number is not exaggerated at all. That is exactly the problem that Ansible was built to solve. And once you understand what it does and how it thinks, you will wonder how anyone managed large infrastructure without it.

Jun 18, 202612 min read
You Have 400 Servers to Configure. Now What? Let Ansible Do the Work.

Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

This post wraps up the core Docker Swarm curriculum by covering four important topics that complete the picture of production-ready containerized deployments. We start with Docker Secrets, which solves the real-world problem of passing sensitive credentials into containers without hardcoding them. We then look at Docker Stack, which is how you run multi-service Docker Compose files across a Swarm cluster instead of a single host. After that we cover the distinction between replicated and global services, which is a concept that appears in Kubernetes as well. We close with a look at Portainer for those who prefer a visual interface, and a brief introduction to distroless images. Each of these topics builds on everything covered so far. If you have your Swarm cluster running, you can follow along with every command shown here.

Jun 17, 202614 min read
Building Production-Ready Docker Deployments with Secrets, Stacks, and Distroless Images

Beyond One Server: Solving Docker Scaling with Swarm and Container Networks

This post covers three separate but deeply connected topics. We start by finishing what was started with Docker Hub, pushing all four bank service images to a remote registry so they survive beyond any single machine. From there, we identify a real architectural problem with single-host Docker deployments and introduce Docker Swarm as the solution. Finally, we close with Docker networking, explaining how containers communicate with each other both on the same host and across different hosts. By the end of this article, you will understand how to push and pull images from Docker Hub, how to set up a multi-node Docker Swarm cluster, how to create and scale services across that cluster, what self-healing means in practice, and how Docker networking works under the hood.

Jun 17, 202618 min read
Beyond One Server: Solving Docker Scaling with Swarm and Container Networks

Docker Compose in Action: Multi-Container Apps, Nginx Load Balancing & Docker Hub

We built four containerized microservices for an bank application in the last post. Internet banking, mobile banking, insurance, and loans, each running in its own container, each exposed on a separate port. The setup worked. But the process of building and running each container individually by hand was repetitive, error-prone, and simply not practical at scale. This post introduces Docker Compose, and by the end, you will understand not just how to use it, but why it exists, what its real limitations are, how to combine it with Nginx to build a working high availability architecture, and how to push your images to Docker Hub so they are available beyond your local machine. There is also a hands-on project included here that builds a Flask-based Python application behind an Nginx load balancer, which you are expected to complete as a practical exercise.

Jun 16, 202617 min read
Docker Compose in Action: Multi-Container Apps, Nginx Load Balancing & Docker Hub
S

Sai Praneeth's Blogs

37 posts

From SDLC and Agile to DevOps and CI/CD, this blog is where I share structured technical notes, concepts and practical insights in Cloud and DevOps engineering.