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
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
For Mac and Linux users:
source venv/bin/activate
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 insidevenv
.
With this we can go ahead and install the necessary dependencies:
pip install flask black
pip freeze > requirements.txt
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
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"
}
At this point, your project structure should look like the following:
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
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
)
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"]
This Dockerfile takes care of creating three image targets.
Thebase
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.
Thedebug
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 theprod
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
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
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
Define the exposed ports, where
5000
is the application port and5678
is de debug port to connect with vscode.
- Lines 13 and 14
volumes:
- .:/usr/src/app
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" ]
This is a very important one!
In this line, we run a multiprocess script where first we run thedebugpy
that starts listening to the port5678
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
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"
}
]
}
]
}
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:
Now if you try to access the URL http://localhost:5000/api/test
it should stop in the breakpoint you included.
Yay, we did it!
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.
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)
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.
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...
Ty you're great
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?
"-m", "app", "--wait-for-client", "--multiprocess",
is it really necessary? it was crashing the execution today.
great tutorial
Hey Diogo!
No, this is not required, It just makes the execution of the app await for the debugger connection.
Kindly fix this
for windows
.\venv\Scripts\activate
you wrote.
./venv/Scripts/activate
This just saved my life from putting logs at random places to debug flask running in Docker.