DEV Community

Cover image for Dockerize a Flask app and debug with VSCode
Thiago Pacheco
Thiago Pacheco

Posted on

Dockerize a Flask app and debug with VSCode

Introduction

If you had tried setting up the VSCode debugger with docker before, you might have seen that it can be a bit more complicated than it seems at first. So in this tutorial, I want to share a way to set up a development environment with Flask, Docker and VSCode.

You can find the source code here.

Project setup

Let's start by creating the initial folders and installing the necessary dependencies:

mkdir flask-api
cd flask-api
python -m venv venv
Enter fullscreen mode Exit fullscreen mode

Once created the files and initiated the virtual env, let's start the virtual env and install the necessary packages:

For Windows users:

./venv/Scripts/activate
Enter fullscreen mode Exit fullscreen mode

For Mac and Linux users:

source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

The virtual env is used to isolate the installation of the packages, so whenever you try to install anything with pip these new dependencies are added in the lib folder inside venv.

With this we can go ahead and install the necessary dependencies:

pip install flask black
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Now we can create a basic flask api, starting by defining the base folder and some simple routes.
Let's open the flask-api project with VSCode and create a package called api at the root of the project:

code flask-api
mkdir api
touch api/__init__.py
Enter fullscreen mode Exit fullscreen mode

As we installed black as our file formatter, let's setup a configuration to let VSCode make use of it. Create a .vscode folder add a file called settings.json with the following content:

// .vscode/settings.json
{
    "python.formatting.provider": "black"
}
Enter fullscreen mode Exit fullscreen mode

At this point, your project structure should look like the following:
Initial setup

We can now include the content of the __init__.py file:

from flask import Flask, jsonify


def create_app():
    app = Flask(__name__)

    @app.route("/api/test", methods=["GET"])
    def sample_route():
        return jsonify({"message": "This is a sample route"})

    return app
Enter fullscreen mode Exit fullscreen mode

Here we have a simple method that creates a flask app and includes a sample route. This method can be useful for extending this and mocking it in the future for tests.

Include a file called app.py at the root of the project with the following content:

from api import create_app

app = create_app()

if __name__ == "__main__":
    app.run(
        debug=True,
        host='0.0.0.0',
        port=5000
    )
Enter fullscreen mode Exit fullscreen mode

This file will be the entry point of our app.

At this point, we are able to press F5 in VSCode, select python Flask and use app.py as the entrypoint. The application should run successfully.
You can check the results in the URL: http://127.0.0.1:5000/api/test

Dockerize the app

We already have an application up and running, so now let's stop it and get started with dockerizing it.

The first step is to create our Dockerfile at the root of the project with the content:

FROM python:3.6 as base
# Base image to be reused
LABEL maintainer "Thiago Pacheco <hi@pacheco.io>"
RUN apt-get update
WORKDIR /usr/src/app
COPY ./requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV="docker"
ENV FLASK_APP=app.py
EXPOSE 5000

FROM base as debug
# Debug image reusing the base
# Install dev dependencies for debugging
RUN pip install debugpy
# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE 1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED 1

FROM base as prod
# Production image
RUN pip install gunicorn
COPY . .
CMD ["gunicorn", "--reload", "--bind", "0.0.0.0:5000", "app:app"]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile takes care of creating three image targets.
The base is where we put the necessary config for all the following targets. Here we create the work directory folder, install all the project dependencies, and set up the flask environment variables.
The debug will be used in the docker-compose.yaml file to setup our dev environment and it covers installing the debugpy dependency and the dev environment variables.
And finally the prod target will be used for the production release, which will cover installing gunicorn, copying all the project files, and setting up the entry point.

Create a docker-compose.yaml file with the following content:

version: "3.7"

services:
  flask-api:
    image: thisk8brd/flask-api
    container_name: flask-api
    build:
      context: .
      target: debug
    ports:
      - 5000:5000
      - 5678:5678
    volumes:
      - .:/usr/src/app
    environment:
      - FLASK_DEBUG=1
    entrypoint: [ "python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "-m", "app",  "--wait-for-client", "--multiprocess", "-m", "flask", "run", "-h", "0.0.0.0", "-p", "5000" ]
    networks:
      - flask-api

networks:
  flask-api:
    name: flask-api

Enter fullscreen mode Exit fullscreen mode

With this docker-compose.yaml file we are configuring the flask-api service. Let's break this down to explain it:

  • Lines 5 to 9
    image: thisk8brd/flask-api
    container_name: flask-api
    build:
      context: .
      target: debug
Enter fullscreen mode Exit fullscreen mode

Here we define that we want to build the image based on our Dockerfile config and set the target as debug.

  • Lines 10 to 12
    ports:
      - 5000:5000
      - 5678:5678
Enter fullscreen mode Exit fullscreen mode

Define the exposed ports, where 5000 is the application port and 5678 is de debug port to connect with vscode.

  • Lines 13 and 14
    volumes:
      - .:/usr/src/app
Enter fullscreen mode Exit fullscreen mode

Here we point the container working dir to our local project dir, which allows us to use the flask hot-reload functionality.

  • Line 18
entrypoint: [ "python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "-m", "app",  "--wait-for-client", "--multiprocess", "-m", "flask", "run", "-h", "0.0.0.0", "-p", "5000" ]
Enter fullscreen mode Exit fullscreen mode

This is a very important one!
In this line, we run a multiprocess script where first we run the debugpy that starts listening to the port 5678 and after this, we run the flask application. This listener is used to connect the container with the VSCode and use its debug functionality.

Now we are already able to run the application.

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

The application should be up and running at http://localhost:5000/api/test

Setup vscode debug

We are almost done with the setup.
Now for the last step, we can create the VSCode launch script to connect with our container.

Create a file called launch.json under the .vscode folder with the following content:

{
  "configurations": [
    {
      "name": "Python: Remote Attach",
      "type": "python",
      "request": "attach",
      "port": 5678,
      "host": "0.0.0.0",
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "/usr/src/app"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

With your application running, just press F5 in the VSCode. You should get the debug menu at the top of your editor and be able to start coding and debugging.

Go to your api/__init__.py file and add a breakpoint in the return of the create_app method, like so:
Alt Text

Now if you try to access the URL http://localhost:5000/api/test it should stop in the breakpoint you included.
Debug vscode

Yay, we did it!

Chandler success

Now we are able to debug and use the hot-reload functionality while working on a dockerized Flask application!

If you find some issues with the hot-reload functionality, you may need to disable the Uncaught Exceptions checkbox under the Run tab in VSCode, in the Breakpoints section.
Alt Text

I hope you may find this tutorial useful. Let me know in the comments if that helped you or if you have any doubts.

You can find the source code here.

Top comments (9)

Collapse
 
icecoffee profile image
Atulit Anand • Edited

Why do we use this when we have virtual environment for python apps? I mean after all both are just techniques to isolate the app so it can run independently.
Any how cheers mate for the job well done.

Collapse
 
pacheco profile image
Thiago Pacheco

Yes, they both isolate the code but they have different purposes.
The virtual env takes care of the python dependencies only, but the docker container will take care of creating the entire environment (but using your current system resources), similar to creating a virtual machine in your computer and installing all the necessary dependencies like a specific Python version for example.
This is especially good because it removes that issue of incompatible software versions between coworkers' computers and the prod, staging and dev environments.
Basically, you have a production-ready setup.
Linode has a great article about why and when to use docker, maybe this could be a good help to you:
linode.com/docs/guides/when-and-wh...

Collapse
 
icecoffee profile image
Atulit Anand

Ty you're great

Collapse
 
bizzguy profile image
James Harmon • Edited

Once I started using Docker, VS Code no longer recognized new packages entered in requirements.txt. The app itself worked fine, but VS code showed error messages. Is there any way to get VS code to recognize newly added packages?

Collapse
 
diogosimao profile image
Diogo Simão

"-m", "app", "--wait-for-client", "--multiprocess",

is it really necessary? it was crashing the execution today.

Collapse
 
diogosimao profile image
Diogo Simão

great tutorial

Collapse
 
pacheco profile image
Thiago Pacheco

Hey Diogo!
No, this is not required, It just makes the execution of the app await for the debugger connection.

Collapse
 
aymhenry profile image
Ayman Henry

Kindly fix this

for windows
.\venv\Scripts\activate

you wrote.
./venv/Scripts/activate

Collapse
 
atkumar profile image
Ashutosh Kumar

This just saved my life from putting logs at random places to debug flask running in Docker.