Create a Docker multi-container Flask app

man facing three computer monitors while sitting

Docker has been around for 7 years and there are still developers out there that haven’t used it. 

The traditional way developers work locally usually is by installing something like XAMPP or WAMP if you are working with PHP maybe even running a Virtualbox somehow.  This is okay, but have you ever run into issues where code doesn’t work the same as on a server? What about developers running the same code on their machines but experiencing different issues with the same code.

Even when deploying code to a live server you would end up with a few different issues, which you would then need to debug on the live server to get it working. Then the reverse sometimes happens on local vs live again. 

So what is the right way? 

Docker is the answer. You can build a custom Docker image that everyone on your team can use in combination with Docker-compose. This way you can all work from the same types of distros and setups.

For example, you can build a simple Dockerfile where you can specify which base image you want to use. I like using Debian and currently use Debian buster in most of my projects. I also like using Ubuntu, it depends on the project and how I feel the day.

Then you will specify what else you want part of that image seeing that Debian pretty much comes as a minimal image. You would probably want to install something like Vim, Curl and Wget on there.

You could decide to install Apache and PHP on there, but then again you have to decide should I rather use the PHP image which uses Debian buster for example php:7.3-apache-buster

What about Python? Instead of using the base Debian image, I recommend using the python image: python:3.7-buster.

Now, this article did not start by telling you what Docker is, or a 101 crash course on Docker. If you don’t know what Docker is, I suppose now is a good time to find out, go here.

So let this article rather serve you as a first-time run or maybe even your second run, because you have been Googling and still don’t get it. I will try and explain in as much or little detail as possible. The only way to learn is to get your hands dirty but in a fun way.

What are we going to build?

We are going to build a Dockerfile, then we are going to create a Docker-compose file and combine different images to do different things. See it as running multiple apps on one server.

 We will put the following together :

  1. A MySQL image as a Database
  2. A Python image running Flask.
  3. Nginx which serves as a reverse proxy.

Are you excited yet?

Docker-Compose File Creation.

The first thing we want to start with is creating a docker-compose.yaml file then you add MySQL as our database service. We are not going to use MySQL in this tutorial, but adding it for the purpose of a future tutorial.

version: '3.4'

services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    volumes:
      - db_data_python_flask:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: RandomRoot@213!
      MYSQL_DATABASE:  flask_app
      MYSQL_USER: flask_user
      MYSQL_PASSWORD: RandomRoot@213!
    ports:
      - "33061:3306"

Explanation:

  1. When you look at the above, we create a service called “db” which uses a MySQL image with a specific version tag.
  2. We have a “command” which specifies the default authentication plugin to use for MySQL.
  3. A volume to persist the data so that we don’t lose data when a container restarts or shuts down.
  4. “restart”, to always restart when something goes wrong and try and correct itself.
  5. “environment”, for environment variables within the image to be used in your app. Here we specify the MySQL database credentials to hook up your application to the database.
  6. We set the default MySQL port to a different port as it’s better in my opinion not to expose the default port.

Next, we want to create a Dockerfile for our custom python flask image. This will use a base image, python:3.7-buster image.

The Dockerfile looks like this:

FROM python:3.7-buster


EXPOSE 80/tcp
EXPOSE 443/tcp

RUN apt-get update && apt-get install vim -y

RUN /usr/local/bin/python -m pip install --upgrade pip

WORKDIR /project

COPY flask_app/ /project

RUN pip install -r requirements.txt

EXPOSE 8003/tcp

WORKDIR /project

ENTRYPOINT [ "./start_gunicorn_server.sh" ]

Explanation:

  1. We choose our base image to use. Always use a specific version and not the latest tag to avoid versioning issues you might have in the future.
  2. Expose port 80 and 443 for web traffic
  3. Install VIM for editing files
  4. Upgrade pip to the latest version
  5. Create a project directory
  6. Copy our flask app to the image project directory
  7. Install all the python libraries we require
  8. Expose port 8003 as we are using this port to run our app with Gunicorn
  9. Just make double sure I’m in the project directory
  10. Trigger the Gunicorn shell script to start the server for the flask app

Next, we have a shell script (start_gunicorn_server.sh), which will be used to start the production server on the docker container which runs the following command:

gunicorn app:app -w 2 --threads 2 -b 0.0.0.0:8003

This starts the Gunicorn server for your file app.py with 2 workers and 2 threads on port 8003.

Now back to the docker-compose.yaml file to add another service for the flask app. You can see it in the yaml code below:

  flask_app:
    depends_on:
      - db
    build: .
    ports:
      - "8003:8003"
      - "4431:443"
    restart: always

Explanation:

  1. We create a service called “flask_app”
  2. This service depends on the “db” service
  3. Build the Dockerfile ( local context )
  4. Map ports for the app to run, 8003 which we will proxy to later plus port 4431 mapped to 443 for https. ( we might not even need this but I map it anyways )
  5. We set our restart policy to always restart

Next, we add another service for our Nginx reverse proxy:

  reverse_proxy:
    depends_on:
      - flask_app
    container_name: reverse-proxy
    hostname: reverse
    image: nginx:1.18.0
    ports:
      - "80:80"
      - "443:443"
    restart: always
    volumes:
      - nginx_data:/etc/nginx
      - nginx_data:/etc/ssl/private

Explanation:

  1. Create a service called reverse_proxy
  2. This depends on the “flask_app” service, we created before
  3. We give it a container name of “reverse-proxy”
  4. We use an image that is a Debian buster image for Nginx
  5. We map ports 80 and 443 for HTTP and HTTPS traffic
  6. A restart policy of always
  7. We map to paths to the “nginx_data”

The last part of the docker-compose.yaml file, we add the main volumes we mapped in the services:

volumes:
    db_data_python_flask: {}
    nginx_data: {}

The complete final docker-compose.yaml file should look like this:

version: '3.4'

services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    volumes:
      - db_data_python_flask:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: RandomRoot@213!
      MYSQL_DATABASE:  flask_app
      MYSQL_USER: flask_user
      MYSQL_PASSWORD: RandomRoot@213!
    ports:
      - "33061:3306"

  flask_app:
    depends_on:
      - db
    build: .
    ports:
      - "8003:8003"
      - "4431:443"
    restart: always

  reverse_proxy:
    depends_on:
      - flask_app
    container_name: reverse-proxy
    hostname: reverse
    image: nginx:1.18.0
    ports:
      - "80:80"
      - "443:443"
    restart: always
    volumes:
      - nginx_data:/etc/nginx
      - nginx_data:/etc/ssl/private

volumes:
    db_data_python_flask: {}
    nginx_data: {}

The Flask App

Finally, we set up a simple little flask app for this exercise. I’m not going to go into how to set up Python or Virtualenv, you can find out on your own, see it as homework.

Here are the steps to set up a simple flask app:

In your terminal run “virtualenv venv“, then run “source venv/bin/activate

This will activate the Python virtual environment for you.

Next install flask with the following command:

pip install Flask

You can read more on installing flask here.

Next,  you want to create a folder called  flask_app and inside the folder, a file called app.py.

The code looks like this inside app.py:

from flask import Flask
app = Flask(__name__)
 
@app.route('/')
def home():
    return "Hello you have successfully created a minimal python Flask app with Docker"

Explanation:

  1. We first import the Flask class from the flask library.
  2. Next, we create an instance of this class.
  3. we set the route decorator to / which will be the main URL you land on.
  4. we create a method called home() which returns some text to the user’s browser.

Next, we install Gunicorn

pip install gunicorn 

This we will be used to be able to run Gunicorn in the shell script I mentioned earlier in this article, to start the server.

Starting up our Docker Setup.

Let’s begin this section by actually running docker-compose and making sure that docker creates our containers based on our docker-compose.yaml file.

You can trigger your file with the following command:

docker-compose up -d

This creates the containers for each of the services specified in your compose file. Now when you run “docker ps” you should see all the containers created. Each of them running as a separate service.

Let’s set up our Nginx Config

Now we need to set up our Nginx config. I could have created a separate docker file to add the config from the beginning, but for this tutorial, I wanted to do it afterwards.

Run the following command and look for your Nginx container ID.

docker ps

You should grab the Container ID, my current one is 6013be427253

Next, we want to execute into our container with the following command:

docker exec -it 6013be427253 bash

Do you notice we use the container id to execute into the container? Now that we are inside the container it’s like any other Linux system that runs a Debian server where we can run commands.

We now need to create a config file which will redirect all traffic coming in at port 80 and 443 and redirect it to our flask container running on port 8003.

change the directory to the following path:

cd /etc/nginx/conf.d/

Then create a directory called site-available:

mkdir sites-available

Then change into the sites-available directory:

cd sites-available

Next, we want to install vim to be able to create and edit our config file.

apt update && apt install vim -y

Now let’s create the actual config file with the following command.

vim nginx-live.conf

Now paste the config file contents below.

The config file looks like this:

server {
    listen 80;

    server_name yourdomain.com;
    location / {
        # for localhost use host.docker.internal
        # for localhost Mac use docker.for.mac.localhost
        proxy_pass http://yourdomain.com:8003;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host $server_name;
    }
}

Press Esc and type :wq to save and quit the file. Now please note that if you are going to run this on your local machine you need to use host.docker.internal as your domain name.

Next, we want to go and edit the nginx.conf file and the line which includes all conf files.

Find this line:

include /etc/nginx/conf.d/*.conf;

Change it to:

include /etc/nginx/conf.d/sites-available/*.conf;

Save the file and then reload Nginx so that the new config file is picked up.

nginx -s reload

Now at this time, you should be able to visit your domain name and the flask app home page should show the message from the flask app. If you’re on localhost you can go to http://127.0.0.1

Congrats, you have reached the end of this tutorial. Please leave a comment and give me feedback or ask for help.

Download this project from GitHub

Consider buying me a coffee or becoming a monthly member, this way you will help me to do what I’m passionate about.

Buy Me A Coffee

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.