Containerizing a single monolithic application in Docker is less about building microservices and more about creating a portable, reproducible environment for your existing, single-process application.
Here’s a typical setup to run a Python Flask application, often found in monoliths, inside a Docker container:
app.py (Your Monolithic Application)
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello from the monolith!'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
requirements.txt (Application Dependencies)
Flask==2.3.2
Dockerfile (The Blueprint for Your Container)
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Make port 5000 available to the world outside this container
EXPOSE 5000
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]
Building and Running the Container
-
Build the Docker image: Navigate to the directory containing your
Dockerfile,app.py, andrequirements.txt, and run:docker build -t my-monolith-app .This creates an image named
my-monolith-app. -
Run the Docker container:
docker run -d -p 5001:5000 --name monolith-instance my-monolith-app-d: Runs the container in detached mode (in the background).-p 5001:5000: Maps port 5001 on your host machine to port 5000 inside the container (where your Flask app is listening).--name monolith-instance: Assigns a name to your running container for easy reference.
Now, if you visit http://localhost:5001 in your web browser, you should see "Hello from the monolith!".
The core problem a monolith in Docker solves is environment consistency and portability. Instead of dealing with "it works on my machine" issues due to differing OS versions, installed libraries, or configurations, Docker packages your application and its entire runtime environment into a single, self-contained unit. This makes deployment predictable, whether it’s on your laptop, a staging server, or in production. You’re essentially creating a shoebox for your application that includes everything it needs to run, no matter where you open the box.
Internally, the Dockerfile acts as a script. FROM specifies the base operating system and language runtime. WORKDIR sets the current directory inside the container. COPY brings your application code and dependencies into that directory. RUN pip install executes commands within the container’s build environment to set up dependencies. EXPOSE is documentation for the user, indicating which ports the application inside the container listens on. CMD is the command that executes when the container starts, launching your application.
The real power comes from how this image can be shared and run anywhere Docker is installed. You can easily share the my-monolith-app image with a colleague, and they can run it with the exact same docker run command, guaranteeing the same behavior. This dramatically simplifies development, testing, and deployment workflows.
When you run a container, Docker creates a new isolated filesystem layer for it. Your application code and its dependencies are within this layer. The CMD instruction then starts your application process within this isolated environment. The -p flag on docker run sets up network address translation (NAT) so that traffic hitting a specific port on your host machine is forwarded to the corresponding port inside the container, allowing external access to your application.
A subtle but crucial detail for monoliths is managing state and persistent data. By default, any data written by your application inside the container is lost when the container is removed. To handle this, you’d typically use Docker volumes. For instance, if your monolith stored user uploads in a /app/uploads directory, you’d modify your docker run command:
docker run -d -p 5001:5000 -v monolith_uploads:/app/uploads --name monolith-instance my-monolith-app
Here, -v monolith_uploads:/app/uploads creates or attaches a named volume called monolith_uploads to the /app/uploads directory inside the container. This ensures that data written to /app/uploads persists even if the container is stopped, removed, or replaced, effectively decoupling your application’s data from its lifecycle.
The next logical step after containerizing a monolith is often exploring how to manage multiple such containers or integrate them into a larger orchestration system.