Containerizing the Application with Docker
In the previous article, I prepared the application for containerization by removing the VM-specific assumptions that were embedded in the code.
Two key changes were made:
- The Flask application no longer contains a hardcoded database API hostname.
- The FastAPI database service no longer assumes MongoDB runs on localhost.
These changes allow configuration to be supplied dynamically through environment variables, which is a core requirement for containerized applications.
With that foundation in place, we can now move to the next step of the modernization journey: building Docker images for the application components.
Containerization Strategy
The original VM architecture separated the system into three tiers:
- Web tier (Nginx)
- Application tier (Flask)
- Database tier (FastAPI + MongoDB)
When moving to containers, we will maintain the same logical separation.
Each component becomes its own container.
Browser
|
v
Nginx Container
|
v
Flask Application Container
|
v
FastAPI Database Container
|
v
MongoDB ContainerBreaking the system into containers provides several advantages:
- each component can scale independently
- services can be updated without affecting the entire stack
- infrastructure becomes portable across environments
Containerizing the Applications
Now let's talk about containerizing that actual application layers and the required changes.
Containerizing the Flask Application
The Flask application lives in the following directory from the original VM project:
OVA Environment-Without DB-Load-Balanced/
App VM/The main application entry point is:
App VM/app.pyTo containerize this service, we create a Dockerfile in this directory.
Flask Dockerfile
Create a file named:
App VM/DockerfileContents:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["gunicorn","-w","2","-b","0.0.0.0:8080","app:app"]What this Dockerfile does
Uses a lightweight Python base image.
Sets /app as the working directory.
Installs dependencies from requirements.txt.
Copies the application source code.
Exposes port 8080.
Starts the Flask application using Gunicorn.
This replaces the VM startup logic previously defined in:
gunicorn.startContainerizing the Database API
The backend API service lives in the updated database directory:
demo-3-tier-app/DB VM/The entry point for the FastAPI service is:
DB VM/app.pyTo containerize this service, we create another Dockerfile.
FastAPI Dockerfile
Create the file:
DB VM/DockerfileContents:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn","-k","uvicorn.workers.UvicornWorker","-w","2","-b","0.0.0.0:8000","app:app"]Why Gunicorn with Uvicorn workers?
FastAPI applications run on ASGI servers.
Using Gunicorn with Uvicorn workers provides:
- production-grade process management
- better concurrency
- compatibility with containerized environments
Using an Official MongoDB Image
Unlike the other services, we do not need to build our own image for MongoDB. Docker Hub already provides an official MongoDB image. Later, when we introduce Docker Compose, we will simply use:
mongo:6This eliminates the need to manage database installation manually, which was required in the VM deployment.
Removing the VM Startup Model
In the VM environment, the application relied on several startup scripts such as:
customize-app1-vm.start
customize-app2-vm.start
customize-db-vm.start
customize-web1-vm.start
customize-web2-vm.start
These scripts were responsible for:
- injecting configuration
- updating application files
- wiring service endpoints
- starting application services
In the container model, those responsibilities move to different places.
VM Startup Logic | Container Equivalent |
|---|---|
Inject hostnames | Environment variables |
Modify config files | Runtime configuration |
Start services | Dockerfile CMD |
Discover services | Container DNS |
This shift removes the need for most deployment scripts.
Verifying the Images Build
At this stage, we are not yet running the full stack. The goal of this phase is simply to verify that the container images build correctly. From the project root, we can build the images manually.
Build the Flask image
docker build -t demo-flask-app ./App\ VMBuild the database API image
docker build -t demo-db-api ./DB\ VMIf both images build successfully, the application is ready for the next phase.
Note: I had to update the motor library in Python. I used it to connect to MongoDB. The earlier version was 2.5. I had to change it to 3.7.1 (latest) to make it compatible with Python 3.11.1. Otherwise the application will fail to start later.
DB VM/requirements.txt
Change motor == 2.5 to motor == 3.7.1
What We Achieved in This Step
In this article we accomplished the first major transformation of the application. The Flask and FastAPI services are no longer tied to VM environments. Instead they are packaged as portable container images. This means the application can now run:
- on a developer laptop
- on any Docker host
- inside CI pipelines
- on Kubernetes clusters
However, the containers are still independent at this stage. We have not yet connected the services together.
What Comes Next
In the next article, we will take the next step and run the entire application stack using Docker Compose. This will allow us to launch the complete system — including MongoDB, the API service, and the Flask application — with a single command.
Docker Compose will also handle:
- container networking
- environment variables
- service dependencies
This is where the application begins to behave like a true containerized platform.
Series Progress
Part 1 — Understanding the Original 3-Tier Architecture
Part 2 — Preparing the Application for Containerization
Part 3 — Containerizing the Application with Docker
Next:
Part 4 — Running the Application with Docker Compose
This is where we will finally run the entire stack together using containers.