Create a Docker multi-container Flask app
Tags: docker, python, flask
Published: 2020-06-17 17:42:24

<!-- wp:group {"backgroundColor":"very-light-gray"} -->
<div class="wp-block-group has-very-light-gray-background-color has-background"><div class="wp-block-group__inner-container"><!-- wp:html -->
<h3 style="padding-left:10px; padding-top:10px;">Table of Contents</h3>
<!-- /wp:html -->

<!-- wp:html -->
<ul style="padding-bottom:10px;"><li><a href="#what-are-we-going-to-build" class="rank-math-link">What are we going to build?</a></li><li><a href="#docker-compose-file-creation" class="rank-math-link">Docker-Compose File Creation</a></li><li><a href="#the-flask-app" class="rank-math-link">The Flask App</a></li><li><a href="#starting-up-our-docker-setup" class="rank-math-link">Starting up our Docker Setup</a></li><li><a href="#lets-set-up-our-nginx-config" class="rank-math-link">Let’s set up our Nginx Config</a></li></ul>
<!-- /wp:html --></div></div>
<!-- /wp:group -->

<!-- wp:paragraph -->
<p>Docker has been around for 7 years and there are still developers out there that haven't used it.&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The traditional way developers work locally usually is by installing something like&nbsp;<strong>XAMPP&nbsp;</strong>or<strong>&nbsp;WAMP&nbsp;</strong>if you are working with PHP<strong>&nbsp;</strong>maybe even running a<strong>&nbsp;Virtualbox&nbsp;</strong>somehow.<strong>&nbsp;</strong>&nbsp;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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>So what is the right way?&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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&nbsp;<a target="_blank" href="https://hub.docker.com/_/php" rel="noreferrer noopener">php:7.3-apache-buster</a></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>What about Python? Instead of using the base&nbsp;<a target="_blank" href="https://hub.docker.com/_/debian" rel="noreferrer noopener">Debian</a>&nbsp;image, I recommend using the python image:&nbsp;<a target="_blank" href="https://hub.docker.com/_/python" rel="noreferrer noopener">python:3.7-buster</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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&nbsp;<a target="_blank" href="https://www.docker.com/" rel="noreferrer noopener">here</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 id="what-are-we-going-to-build"><strong>What are we going to build?</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>&nbsp;We will put the following together :</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>A MySQL image as a Database</li><li>A Python image running Flask.</li><li>Nginx which serves as a reverse proxy.</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Are you excited yet?</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 id="docker-compose-file-creation">Docker-Compose File Creation.</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>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"</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>Explanation:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>When you look at the above, we create a service called "db" which uses a MySQL image with a specific version&nbsp;<a target="_blank" href="https://docs.docker.com/engine/reference/commandline/tag/" rel="noreferrer noopener">tag</a>.</li><li>We have a "command" which specifies the default authentication plugin to use for MySQL.</li><li>A volume to persist the data so that we don't lose data when a container restarts or shuts down.</li><li>"restart", to always restart when something goes wrong and try and correct itself.</li><li>"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.</li><li>We set the default MySQL port to a different port as it's better in my opinion not to expose the default port.</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Next, we want to create a Dockerfile for our custom python flask image. This will use a base image,&nbsp;<a rel="noreferrer noopener" target="_blank" href="https://hub.docker.com/_/python/">python</a>:3.7-buster image.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The Dockerfile looks like this:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>FROM python:3.7-buster


EXPOSE 80/tcp
EXPOSE 443/tcp

RUN apt-get update &amp;&amp; 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" ]</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>Explanation:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>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.</li><li>Expose port 80 and 443 for web traffic</li><li>Install VIM for editing files</li><li>Upgrade pip to the latest version</li><li>Create a project directory</li><li>Copy our flask app to the image project directory</li><li>Install all the python libraries we require</li><li>Expose port 8003 as we are using this port to run our app with&nbsp;<a target="_blank" href="https://gunicorn.org/" rel="noreferrer noopener">Gunicorn</a></li><li>Just make double sure I'm in the project directory</li><li>Trigger the Gunicorn shell script to start the server for the flask app</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Next, we have a shell script (<strong>start_gunicorn_server.sh</strong>), which will be used to start the production server on the docker container which runs the following command:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>gunicorn app:app -w 2 --threads 2 -b 0.0.0.0:8003</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>This starts the Gunicorn server for your file app.py with 2 workers and 2 threads on port 8003.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now back to the&nbsp;<strong>docker-compose.yaml</strong>&nbsp;file to add another service for the flask app. You can see it in the yaml code below:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>  flask_app:
    depends_on:
      - db
    build: .
    ports:
      - "8003:8003"
      - "4431:443"
    restart: always</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>Explanation:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>We create a service called "flask_app"</li><li>This service&nbsp;<a target="_blank" href="https://docs.docker.com/compose/compose-file/#depends_on" rel="noreferrer noopener">depends on</a>&nbsp;the "db" service</li><li>Build the Dockerfile ( local context )</li><li>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 )</li><li>We set our&nbsp;<a target="_blank" href="https://docs.docker.com/compose/compose-file/#restart" rel="noreferrer noopener">restart</a>&nbsp;policy to always restart</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Next, we add another service for our Nginx reverse proxy:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>  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</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>Explanation:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>Create a service called reverse_proxy</li><li>This depends on the "flask_app" service, we created before</li><li>We give it a container name of "reverse-proxy"</li><li>We use an image that is a Debian buster image for Nginx</li><li>We map ports 80 and 443 for HTTP and HTTPS traffic</li><li>A restart policy of always</li><li>We map to paths to the "nginx_data"</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>The last part of the docker-compose.yaml file, we add the main <a href="https://docs.docker.com/compose/compose-file/#volumes" class="rank-math-link">volumes</a> we mapped in the services:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>volumes:
    db_data_python_flask: {}
    nginx_data: {}</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The complete final docker-compose.yaml file should look like this:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>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: {}</code></pre>
<!-- /wp:code -->

<!-- wp:heading -->
<h2 id="the-flask-app">The Flask App</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Finally, we set up a simple little flask app for this exercise. I'm not going to go into how to set up&nbsp;<a rel="noreferrer noopener" target="_blank" href="https://lmddgtfy.net/?q=installing%20python%203.7">Python</a>&nbsp;or&nbsp;<a rel="noreferrer noopener" target="_blank" href="https://virtualenv.pypa.io/en/stable/installation.html">Virtualenv</a>, you can find out on your own, see it as homework.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here are the steps to set up a simple flask app:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>In your terminal run "<em><strong>virtualenv venv</strong></em>", then run "<em><strong>source venv/bin/activate</strong></em>"</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This will activate the Python virtual environment for you.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next install flask with the following command:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>"<strong>pip install Flask</strong>"</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>You can read more on installing flask&nbsp;<a target="_blank" href="https://flask.palletsprojects.com/en/1.1.x/installation/" rel="noreferrer noopener">here</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Next,&nbsp;</strong>&nbsp;you want to create a folder called &nbsp;<em><strong>flask_app</strong> and inside the folder, a</em>&nbsp;file called&nbsp;<em><strong>app.py</strong></em>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The code looks like this inside <strong><em>app.py</em></strong>:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>from flask import Flask
app = Flask(__name__)
 
@app.route('/')
def home():
    return "Hello you have successfully created a minimal python Flask app with Docker"</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>Explanation</strong>:</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>We first import the Flask class from the flask library.</li><li>Next, we create an instance of this class.</li><li>we set the route decorator to / which will be the main URL you land on.</li><li>we create a method called home() which returns some text to the user's browser.</li></ol>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Next, we install&nbsp;<a rel="noreferrer noopener" target="_blank" href="https://docs.gunicorn.org/en/stable/install.html">Gunicorn</a></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>pip install gunicorn </code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 id="starting-up-our-docker-setup">Starting up our Docker Setup.</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>You can trigger your file with the following command:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>docker-compose up -d</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 id="lets-set-up-our-nginx-config">Let's set up our Nginx Config</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Run the following command and look for your Nginx container ID.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>docker ps</code></pre>
<!-- /wp:code -->

<!-- wp:image {"id":66,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://teachingyou.net/wp-content/uploads/2020/06/Screenshot-2020-06-08-at-19.39.36-1024x105.png" alt="" class="wp-image-66"/></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>You should grab the Container ID, my current one is <strong>6013be427253</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next, we want to execute into our container with the following command:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>docker exec -it 6013be427253 bash</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>change the directory to the following path:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>cd /etc/nginx/conf.d/</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Then create a directory called site-available:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>mkdir sites-available</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Then change into the sites-available directory:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>cd sites-available</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Next, we want to install vim to be able to create and edit our config file.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>apt update &amp;&amp; apt install vim -y</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now let's create the actual config file with the following command.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>vim nginx-live.conf</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now paste the config file contents below.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The config file looks like this:</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>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;
    }
}</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next, we want to go and edit the nginx.conf file and the line which includes all conf files.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>Find this line:

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

Change it to:

include /etc/nginx/conf.d/sites-available/*.conf;</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Save the file and then reload Nginx so that the new config file is picked up.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>nginx -s reload</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>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</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Congrats, you have reached the end of this tutorial. Please leave a comment and give me feedback or ask for help.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><a href="https://github.com/holla22/docker-flask-tutorial" class="rank-math-link">Download this project from GitHub</a></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This article was originally posted on <a href="https://teachingyou.net/docker-multi-container-flask-app/" class="rank-math-link">TeachingYou.net</a></p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Consider buying me a coffee or becoming a monthly member, this way you will help me to do what I'm passionate about.</h2>
<!-- /wp:heading -->

<!-- wp:html -->
<a href="https://www.buymeacoffee.com/z33man" rel="nofollow"><img src="https://camo.githubusercontent.com/be06971baed9105260e0ed5c03746108c30b527f/68747470733a2f2f63646e2e6275796d6561636f666665652e636f6d2f627574746f6e732f64656661756c742d6f72616e67652e706e67" alt="Buy Me A Coffee" height="41" width="174" data-canonical-src="https://cdn.buymeacoffee.com/buttons/default-orange.png" style="max-width:100%;"></a>
<!-- /wp:html -->