TL;DR
Here's the docker-compose.yml file to dockerize a Flask application, a Mongo database and a schedule service:
version: '3.8'
services:
app:
container_name: my_app_${ENV}
build:
context: .
ports:
- 8001:8001
depends_on:
- mongo_db
networks:
- my_network
command: ["flask", "run", "--host", "0.0.0.0", "--port", "8001"]
env_file: .env
schedule_service:
container_name: schedule_service_${ENV}
build:
context: ./scheduler
volumes:
- ./common:/app/common
- ./db:/app/db
networks:
- my_networknetwork
command: ["python", "-u", "scheduler.py"]
env_file: .env
mongo_db:
container_name: mongo_db_${ENV}
image: mongo:4.4.18
restart: always
ports:
- 27017:27017
volumes:
- ./DB-STORAGE/mongo-db-$ENV:/data/db
- ./db/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh
networks:
- my_network
env_file: .env
networks:
my_network:
driver: bridge
Run the following command from your project's root directory docker-compose up --build -d to fire it all up.
What is Docker?
Docker is a tool that helps us package up our apps so they can run smoothly no matter where they're deployed – it's like putting our app in a virtual box that contains everything it needs to work - code, settings, and libraries. These boxes are called "containers," and they're lightweight and easy to move around.
With Docker, there's no such a thing as "but it works on my machine..."
Getting started with Dockerfile and Docker Compose
To spin up a container, we can do it directly from the terminal or we can create a file called Dockerfile, which works like a recipe for our app's container. In the Dockerfile, we specify what our app needs to run, like the programming language and dependencies.
If we want to spin up more containers at once, we can use Docker Compose. Docker Compose reads a configuration file – usually called docker-compose.yml, that describes all the containers our app needs to run and how they should interact.
Why should we dockerize an application?
Dockerizing an application offers several benefits:
- It enhances collaboration and streamlines development workflows – we can work in isolated environments without conflicts, speeding up development and making it easier to onboard new team members
- It makes our app more portable – we can easily move it between different environments without worrying about compatibility issues which simplifies deployment and ensures consistency across different platforms
- It improves scalability and resource management - we can easily start/stop containers instances to accommodate fluctuations in traffic
Dockerizing a Flask application
Let's say we've got a Flask application with four main components:
- network: custom Docker network
- app: our Flask application
- mongo_db: a Mongo database
- schedule: an email schedule service
Let's break the Docker Compose code down to understand these components at parameter level.
network
networks:
quot-network:
driver: bridge
-
networksis used to define custom Docker networks in Docker Compose, it allows us to create separate networks for our services, and it helps facilitate communication between the containers running on those networks -
my_networkis the name of the custom network being defined -
driverspecifies the network driver to be used for the custom network
A few notes on Docker networks:
- The bridge driver is the default network driver in Docker and is suitable for most use cases. It enables communication between containers on the same host and provides automatic DNS resolution for service discovery. Each custom network with the "bridge" driver is isolated from the host's default bridge network and other custom bridge networks
- When using the bridge driver, containers on the same network can communicate with each other using their container names as hostnames (e.g., schedule)
- Using the host network instead of bridge allows a container to share the network namespace with the host system. This means the container shares the host's network stack, and network ports used by the container are directly mapped to the host
app
version: '3.8'
services:
app:
container_name: my_app_${ENV}
build:
context: .
ports:
- 8001:8001
depends_on:
- mongo_db
networks:
- my_network
command: ["flask", "run", "--host", "0.0.0.0", "--port", "8001"]
env_file: .env
-
version: '3.8'specifies the version of the Docker Compose file syntax being used - in this case, it's version 3.8 -
servicesis the top-level key that defines the services/containers that will be managed by Docker Compose -
appthe name of the service/container being defined -
container_namespecifies the custom name for the container that will be created based on this service. The variable${ENV}dynamically sets the suffix based on an environment variable. For example, if the value of${ENV}is "production", the container name will bemy_app_production.- The
.envfile should be placed at the root of the project directory next to ourdocker-compose.yml
- The
-
buildindicates that the service will be built using a Dockerfile located in the current directory (denoted by.). Thecontextparameter defines the build context, which means the current directory and its subdirectories will be used to find the necessary files for the build -
portsexposes ports from the container to the host - it will map port8001from the host to port8001in the container, that means any traffic coming to port8001on the host will be forwarded to port8001of the container -
depends_onspecifies that this service depends on another service calledmongo_db. It ensures that themongo_dbis up and running before this service starts -
networksattaches the service to a pre-existing Docker network, this allows both theappservice and other services connected to communicate with each other -
commandoverrides the default command in the Dockerfile that would be executed when the container starts. The app will run with the following parameters:flask run --host 0.0.0.0 --port 8001, meaning Flask will listen on all available network interfaces (0.0.0.0) and port8001- When we define the
commandparameter in the Docker Compose file for a service, it takes precedence over the defaultCMDcommand specified in the Dockerfile
- When we define the
-
env_filespecifies the file from which environment variables should be read and passed to the container
For reference, this is the service's Dockerfile:
FROM python:3.8-slim-buster as base
# Create app directory
WORKDIR /app
# Install Python requirements first so it's cached
COPY ./requirements.txt .
RUN pip3 install -r requirements.txt
# Copy Flask project to container
COPY . .
# Set Flask configurations
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
##############
FROM base as DEV
RUN pip3 install debugger
RUN pip3 install debugpy
# Define Flask environment (production is default)
ENV FLASK_ENV=development
CMD ["python", "-m", "debugpy",\
"--listen", "0.0.0.0:5678",\
"--wait-for-client",\
"-m", "flask", "run", "--host", "0.0.0.0", "--port", "8000"]
##############
FROM base as PROD
CMD ["flask", "run"]
We won't get into how to deploy it to production in this article but I wanted to quickly mention that using Gunicorn - a popular WSGI (Web Server Gateway Interface) HTTP server for Python web applications is probably a good idea:
# Run the app with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8001", "our_app:app"]
mongo db
mongo_db:
container_name: mongo_db_${ENV}
image: mongo:4.4.18
restart: always
ports:
- 27017:27017
volumes:
- ./DB-STORAGE/mongo-db-$ENV:/data/db
- ./db/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh
networks:
- my_network
env_file: .env
-
container_namewell, we already know :) -
imagespecifies the Docker image to be used for the container. In this case, it's using the official MongoDB image with version4.4.18from Docker Hub -
restartindicates the restart policy for the container -alwaysmeans the container will be automatically restarted if it exits, regardless of the exit status -
portsmaps port27017from the host to port27017in the container (default MongoDB port)- Usually, it's best to keep our database private and only let other services (containers) access it. But for development purposes, we can expose it and use tools like MongoDB Compass
-
volumesmounts directories or files from the host into the container. This is used to persist data and configuration files-
./DB-STORAGE/mongo-db-$ENV:/data/dbwill mount a host directory named./DB-STORAGE/mongo-db-$ENVinto the/data/dbdirectory inside the container. This allows the MongoDB data to be stored persistently on the host filesystem -
./db/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.shwill mounts theinit-mongo.shfile from the host into the/docker-entrypoint-initdb.d/init-mongo.shpath in the container which is the official MongoDB Docker image entry point.
-
networksattaches the service to a pre-existing Docker network called "my_network". This allows the MongoDB service and other services connected to "my_network" to communicate with each other.env_filethe environment variables from.envfile will be used in the entrypoint script (init-mongo.sh) to configure MongoDB settings during startup
This is the init-mongo.sh file:
mongo -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin <<EOF
use my_db;
db.createUser({
user: "$MONGO_USER",
pwd: "$MONGO_PASSWORD",
roles:
[
{
role: "readWrite",
db: "my_db"
},
{
role: "dbAdmin",
db: "my_db"
}
]
});
If we want our MongoDB user to have both read/write access to an existing database and the ability to create collections and documents, we should grant it both the
readWriterole and thedbAdminrole.
schedule
schedule_service:
container_name: schedule_service_${ENV}
build:
context: ./schedule
volumes:
- ./common:/app/common
- ./db:/app/db
networks:
- my_network
command: ["python", "-u", "schedule.py"]
env_file: .env
- The
contextparameter defines the build context, which means the ./schedule directory and its subdirectories will be used to find the necessary files for the build
When we're only using a Dockerfile, we can specify the Dockerfile's location and the context (root directory) but when we're working with Docker Compose, we can only specify the context, which means we cannot go up in the directory from where the Dockerfile is
docker build -f $RelativePathToSourceCode -t $AppName .- in that case-f $RelativePathToSourceCodedefines where ourDockerfileis but the.at the end defines the context (root directory)
- In this case, we still want to use some common packages from the application, so we can map them using
volumes - We already know that
commandoverrides the default command that would be executed when the container starts. In this case, the script will be executed with the Python interpreter in unbuffered mode (-uflag) to ensure that the output is immediately displayed in the container logs
In summary, the schedule_service service in the Docker Compose file builds a container from the ./schedule directory, mounts specific directories from the host into the container, and runs the Python script schedule.py as the main command for the container. Additionally, it connects the container to the my_network for communication with other services on the same network and reads environment variables from the .env file.
Firing it all up
Now that we have everything set up, it's time to fire it all up.
Running docker-compose build --no-cache followed by docker-compose up is the best way to ensure that our Docker containers are built from scratch without using any cached layers from previous builds.
Alternatively, using docker-compose up --build will rebuild the images for all services, ignoring any previously cached layers. This ensures that we have the latest version of our application and all its dependencies.
The docker-compose down command both stops and removes containers associated with our Docker Compose project, but the exact behavior depends on the options used with the command.
By default, docker-compose down does the following:
- Stops all the containers defined in our
docker-compose.ymlfile that are currently running as part of the project. The containers are gracefully stopped, and their resources are released - After stopping the containers,
docker-compose downalso removes the containers
To clean it all up, we can use docker-compose down --rmi all.

Top comments (0)