Flask в Docker
Введение | |
Простейшее Flask приложение | |
Проверка | |
Докеризация Flask приложения | |
Docker Compose | |
Спрятать Flask за Nginx | |
Другие статьи о Flask |
Введение
СТАТЬЯ В РАЗРАБОТКЕ
Эта статья именно про запуск в докер контейнере.
Про запуск на виртуальном
хостинге
есть отдельная статья:
«Запуск Flask приложения на хостинге»
Про деплой на своём сервере читайте статью:
«Запуск Flask приложения на сервере Linux (Nginx + Gunicorn)»
Простейшее Flask приложение
Напишем простое приложение, затем запустим его в виртуальном окружении и протестируем с помощью curl
Структура
wired-brain └── product-service ├── src │ └── app.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
from flask import Flask, jsonify, request products = [ {'id': 1, 'name': 'Product 1'}, {'id': 2, 'name': 'Product 2'} ] app = Flask(__name__) # curl -v http://localhost:5000/products @app.route('/products') def get_products(): return jsonify(products) # curl -v http://localhost:5000/product/1 @app.route('/product/<int:id>') def get_product(id): product_list = [product for product in products if product['id'] == id] if len(product_list) == 0: return f'Product with id {id} not found', 404 return jsonify(product_list[0]) # curl --header "Content-Type: application/json" --request POST --data '{"name": "Product 3"}' -v http://localhost:5000/product @app.route('/product', methods=['POST']) def post_product(): # Retrieve the product from the request body request_product = request.json # Generate an ID for the post new_id = max([product['id'] for product in products]) + 1 # Create a new product new_product = { 'id': new_id, 'name': request_product['name'] } # Append the new product to our product list products.append(new_product) # Return the new product back to the client return jsonify(new_product), 201 # curl --header "Content-Type: application/json" --request PUT --data '{"name": "Updated Product 2"}' -v http://localhost:5000/product/2 @app.route('/product/<int:id>', methods=['PUT']) def put_product(id): # Get the request payload updated_product = request.json # Find the product with the specified ID for product in products: if product['id'] == id: # Update the product name product['name'] = updated_product['name'] return jsonify(product), 200 return f'Product with id {id} not found', 404 # curl --request DELETE -v http://localhost:5000/product/2 @app.route('/product/<int:id>', methods=['DELETE']) def delete_product(id): # Find the product with the specified ID product_list = [product for product in products if product['id'] == id] if len(product_list) == 1: products.remove(product_list[0]) return f'Product with id {id} deleted', 200 return f'Product with id {id} not found', 404 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0')
Запуск
Перейдём в директорию product-service и оттуда запустим приложение.
python src/app.py
* Serving Flask app 'app' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://10.10.8.40:5000 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 344-185-542
Проверка
Выполним запросы ко всем эндпойнтам с помощью
curl -v http://localhost:5000/products
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /products HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:15:32 GMT < Content-Type: application/json < Content-Length: 95 < Connection: close < [ { "id": 1, "name": "Product 1" }, { "id": 2, "name": "Product 2" } ] * Closing connection 0
curl -v http://localhost:5000/product/1
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /product/1 HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:16:19 GMT < Content-Type: application/json < Content-Length: 37 < Connection: close < { "id": 1, "name": "Product 1" } * Closing connection 0
curl --header "Content-Type: application/json" --request POST --data '{"name": "Product 3"}' -v http://localhost:5000/product
Note: Unnecessary use of -X or --request, POST is already inferred. * Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > POST /product HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > Content-Type: application/json > Content-Length: 21 > * upload completely sent off: 21 out of 21 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 201 CREATED < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:16:50 GMT < Content-Type: application/json < Content-Length: 37 < Connection: close < { "id": 3, "name": "Product 3" } * Closing connection 0
curl --header "Content-Type: application/json" --request PUT --data '{"name": "Updated Product 2"}' -v http://localhost:5000/product/2
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > PUT /product/2 HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > Content-Type: application/json > Content-Length: 29 > * upload completely sent off: 29 out of 29 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:17:56 GMT < Content-Type: application/json < Content-Length: 45 < Connection: close < { "id": 2, "name": "Updated Product 2" } * Closing connection 0
curl --request DELETE -v http://localhost:5000/product/5
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > DELETE /product/5 HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 404 NOT FOUND < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:18:27 GMT < Content-Type: text/html; charset=utf-8 < Content-Length: 27 < Connection: close < * Closing connection 0 Product with id 5 not found%
curl -v http://localhost:5000/products
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /products HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:18:54 GMT < Content-Type: application/json < Content-Length: 149 < Connection: close < [ { "id": 1, "name": "Product 1" }, { "id": 2, "name": "Updated Product 2" }, { "id": 3, "name": "Product 3" } ] * Closing connection 0
curl --request DELETE -v http://localhost:5000/product/2
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > DELETE /product/2 HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:19:14 GMT < Content-Type: text/html; charset=utf-8 < Content-Length: 25 < Connection: close < * Closing connection 0 Product with id 2 deleted%
curl -v http://localhost:5000/products
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /products HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.9.5 < Date: Wed, 09 Nov 2022 13:19:26 GMT < Content-Type: application/json < Content-Length: 95 < Connection: close < [ { "id": 1, "name": "Product 1" }, { "id": 3, "name": "Product 3" } ] * Closing connection 0
Докеризация Flask приложения
wired-brain └── product-service ├── Dockerfile ├── requirements.txt ├── src │ └── app.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
Dockerfile
# set base image (host OS) FROM python # set the working directory in the container WORKDIR /code # copy the dependencies file to the working directory COPY requirements.txt . # install dependencies RUN pip install -r requirements.txt # copy the content of the local src directory to the working directory COPY src/ . # command to run on container start CMD [ "python", "./app.py" ]
requirements.txt
click==8.1.3 Flask==2.2.2 importlib-metadata==5.0.0 itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.2.2 zipp==3.10.0
docker build -t productservice .
Sending build context to Docker daemon 20.49MB Step 1/6 : FROM python latest: Pulling from library/python 17c9e6141fdb: Pull complete de4a4c6caea8: Pull complete 4edced8587e6: Pull complete a7969cffbf46: Pull complete 74fbfde6af91: Pull complete 16fe51aed899: Pull complete e9ee507bb0de: Pull complete 4d9dbb46d211: Pull complete 3b9b3c4e849c: Pull complete Digest: sha256:fc809ada71c087cec7e2d2244bcb9fba137638978a669f2aaf6267db43e89fdf Status: Downloaded newer image for python:latest ---> 00cd1fb8bdcc Step 2/6 : WORKDIR /code ---> Running in 0f82778197b1 Removing intermediate container 0f82778197b1 ---> c961537f7b3c Step 3/6 : COPY requirements.txt . ---> 59caa4a00092 Step 4/6 : RUN pip install -r requirements.txt ---> Running in 3124be57d5ab Collecting click==8.1.3 Downloading click-8.1.3-py3-none-any.whl (96 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96.6/96.6 kB 3.0 MB/s eta 0:00:00 Collecting Flask==2.2.2 Downloading Flask-2.2.2-py3-none-any.whl (101 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 101.5/101.5 kB 2.3 MB/s eta 0:00:00 Collecting importlib-metadata==5.0.0 Downloading importlib_metadata-5.0.0-py3-none-any.whl (21 kB) Collecting itsdangerous==2.1.2 Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB) Collecting Jinja2==3.1.2 Downloading Jinja2-3.1.2-py3-none-any.whl (133 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.1/133.1 kB 3.0 MB/s eta 0:00:00 Collecting MarkupSafe==2.1.1 Downloading MarkupSafe-2.1.1.tar.gz (18 kB) Preparing metadata (setup.py): started Preparing metadata (setup.py): finished with status 'done' Collecting Werkzeug==2.2.2 Downloading Werkzeug-2.2.2-py3-none-any.whl (232 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 232.7/232.7 kB 2.8 MB/s eta 0:00:00 Collecting zipp==3.10.0 Downloading zipp-3.10.0-py3-none-any.whl (6.2 kB) Building wheels for collected packages: MarkupSafe Building wheel for MarkupSafe (setup.py): started Building wheel for MarkupSafe (setup.py): finished with status 'done' Created wheel for MarkupSafe: filename=MarkupSafe-2.1.1-cp311-cp311-linux_x86_64.whl size=27476 sha256=1fbcd593b974b51b3b8c0a33f4ddaa7233448864e74429f10e86b732f9fd004f Stored in directory: /root/.cache/pip/wheels/96/ee/62/407c247ad088bcb67b530ba3ac1479058c58a651bd6bf09a1f Successfully built MarkupSafe Installing collected packages: zipp, MarkupSafe, itsdangerous, click, Werkzeug, Jinja2, importlib-metadata, Flask Successfully installed Flask-2.2.2 Jinja2-3.1.2 MarkupSafe-2.1.1 Werkzeug-2.2.2 click-8.1.3 importlib-metadata-5.0.0 itsdangerous-2.1.2 zipp-3.10.0 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv [notice] A new release of pip available: 22.3 -> 22.3.1 [notice] To update, run: pip install --upgrade pip Removing intermediate container 3124be57d5ab ---> 706236afd267 Step 5/6 : COPY src/ . ---> 1222e8573555 Step 6/6 : CMD [ "python", "./app.py" ] ---> Running in 6d66349bf9ec Removing intermediate container 6d66349bf9ec ---> 8e8b584fc89a Successfully built 8e8b584fc89a Successfully tagged productservice:latest
docker images | grep -E 'product|REPO'
REPOSITORY TAG IMAGE ID CREATED SIZE productservice latest 8e8b584fc89a 3 minutes ago 952MB
docker run -d -p 5000:5000 productservice
aae35edda96704ea03a5be6fbbcda2d3e2126da4732527c8c2d4d57accac3210
docker ps | grep -E 'product|CONTAINER'
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES aae35edda967 productservice "python ./app.py" 2 minutes ago Up 2 minutes 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp elegant_chaplygin
Проверим, что и сейчас приложение работает
curl -v http://localhost:5000/products
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /products HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.11.0 < Date: Wed, 09 Nov 2022 15:53:13 GMT < Content-Type: application/json < Content-Length: 95 < Connection: close < [ { "id": 1, "name": "Product 1" }, { "id": 2, "name": "Product 2" } ] * Closing connection 0
docker exec -it aae35edda967 bash
root@aae35edda967:/code# ls -l
total 8 -rw-r--r-- 1 root root 2286 Nov 9 13:03 app.py -rw-r--r-- 1 root root 133 Nov 9 14:07 requirements.txt
root@aae35edda967:/code# ls -l /
total 72 drwxr-xr-x 1 root root 4096 Oct 25 09:24 bin drwxr-xr-x 2 root root 4096 Sep 3 12:10 boot drwxr-xr-x 1 root root 4096 Nov 9 15:32 code drwxr-xr-x 5 root root 340 Nov 9 15:36 dev drwxr-xr-x 1 root root 4096 Nov 9 15:36 etc drwxr-xr-x 2 root root 4096 Sep 3 12:10 home drwxr-xr-x 1 root root 4096 Oct 25 09:24 lib drwxr-xr-x 2 root root 4096 Oct 24 00:00 lib64 drwxr-xr-x 2 root root 4096 Oct 24 00:00 media drwxr-xr-x 2 root root 4096 Oct 24 00:00 mnt drwxr-xr-x 2 root root 4096 Oct 24 00:00 opt dr-xr-xr-x 432 root root 0 Nov 9 15:36 proc drwx------ 1 root root 4096 Nov 9 15:31 root drwxr-xr-x 3 root root 4096 Oct 24 00:00 run drwxr-xr-x 1 root root 4096 Oct 25 09:23 sbin drwxr-xr-x 2 root root 4096 Oct 24 00:00 srv dr-xr-xr-x 13 root root 0 Nov 9 15:36 sys drwxrwxrwt 1 root root 4096 Nov 9 15:32 tmp drwxr-xr-x 1 root root 4096 Oct 24 00:00 usr drwxr-xr-x 1 root root 4096 Oct 24 00:00 var
Убедимся в том, что официальный докер обаз Python сдела на основе Debian
root@aae35edda967:/code# cat /etc/issue
Debian GNU/Linux 11 \n \l
Выйти из контейнера можно командой exit
root@aae35edda967:/code# exit
exit
Остановить контейнер
docker stop aae35edda967
aae35edda967
Проверка
docker ps | grep productservice
Docker Compose
Если помимо Flask нашему приложению нужны другие сервисы - можно воспользоваться
Docker Compose
Первым делом в структуру проекта нужно добавить файла
docker-compose.yml
wired-brain ├── docker-compose.yml └── product-service ├── Dockerfile ├── requirements.txt ├── src │ └── app.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
Пример
docker-compose.yml
файла, который просто повторяет, существующий функционал.
Пока что новых сервисов мы не ввели
services: productservice: build: product-service ports: - "5000:5000"
docker-compose build
Building productservice Step 1/6 : FROM python ---> 00cd1fb8bdcc Step 2/6 : WORKDIR /code ---> Using cache ---> 34e2bff6fd00 Step 3/6 : COPY requirements.txt . ---> 83f2837198eb Step 4/6 : RUN pip install -r requirements.txt ---> Running in c6a07e0067f8 Collecting click==8.1.3 Downloading click-8.1.3-py3-none-any.whl (96 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96.6/96.6 kB 1.7 MB/s eta 0:00:00 Collecting Flask==2.2.2 Downloading Flask-2.2.2-py3-none-any.whl (101 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 101.5/101.5 kB 3.3 MB/s eta 0:00:00 Collecting importlib-metadata==5.0.0 Downloading importlib_metadata-5.0.0-py3-none-any.whl (21 kB) Collecting itsdangerous==2.1.2 Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB) Collecting Jinja2==3.1.2 Downloading Jinja2-3.1.2-py3-none-any.whl (133 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.1/133.1 kB 2.6 MB/s eta 0:00:00 Collecting MarkupSafe==2.1.1 Downloading MarkupSafe-2.1.1.tar.gz (18 kB) Preparing metadata (setup.py): started Preparing metadata (setup.py): finished with status 'done' Collecting Werkzeug==2.2.2 Downloading Werkzeug-2.2.2-py3-none-any.whl (232 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 232.7/232.7 kB 2.6 MB/s eta 0:00:00 Collecting zipp==3.10.0 Downloading zipp-3.10.0-py3-none-any.whl (6.2 kB) Building wheels for collected packages: MarkupSafe Building wheel for MarkupSafe (setup.py): started Building wheel for MarkupSafe (setup.py): finished with status 'done' Created wheel for MarkupSafe: filename=MarkupSafe-2.1.1-cp311-cp311-linux_x86_64.whl size=27481 sha256=fb9cb8196e0dd56ac518bb1aca9d1dd7f0cf6d7c234a12f357636203e7cb7a22 Stored in directory: /root/.cache/pip/wheels/96/ee/62/407c247ad088bcb67b530ba3ac1479058c58a651bd6bf09a1f Successfully built MarkupSafe Installing collected packages: zipp, MarkupSafe, itsdangerous, click, Werkzeug, Jinja2, importlib-metadata, Flask Successfully installed Flask-2.2.2 Jinja2-3.1.2 MarkupSafe-2.1.1 Werkzeug-2.2.2 click-8.1.3 importlib-metadata-5.0.0 itsdangerous-2.1.2 zipp-3.10.0 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv [notice] A new release of pip available: 22.3 -> 22.3.1 [notice] To update, run: pip install --upgrade pip Removing intermediate container c6a07e0067f8 ---> a97a95758958 Step 5/6 : COPY src/ . ---> dadd41aeb7d4 Step 6/6 : CMD [ "python", "./app.py" ] ---> Running in 904a9c45da3e Removing intermediate container 904a9c45da3e ---> 7f2e0192915a Successfully built 7f2e0192915a Successfully tagged wired-brain_productservice:latest
docker-compose up -d
Creating network "wired-brain_default" with the default driver Creating wired-brain_productservice_1 ... done
docker ps | grep -E 'product|CONTAINER'
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b96bf25fd7cb wired-brain_productservice "python ./app.py" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp wired-brain_productservice_1
curl -v http://localhost:5000/products
* Trying 127.0.0.1:5000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 5000 (#0) > GET /products HTTP/1.1 > Host: localhost:5000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: Werkzeug/2.2.2 Python/3.11.0 < Date: Wed, 09 Nov 2022 17:03:32 GMT < Content-Type: application/json < Content-Length: 95 < Connection: close < [ { "id": 1, "name": "Product 1" }, { "id": 2, "name": "Product 2" } ] * Closing connection 0
docker-compose down
Stopping wired-brain_productservice_1 ... done Removing wired-brain_productservice_1 ... done Removing network wired-brain_default
Спрятать Flask за Nginx
В предыдущих главах мы обращались к Flask приложению напрямую. Обратите внимание, что при запросах был явно указан порт 5000. Например:
curl -v http://localhost:5000/products
Это нормально в целях обучения, но на продакшене никто так не делает.
В качестве первого шага по приближению нашего приложения к реальному - запустим
так называемый Reverse Proxy на основе
Nginx
Добавим в docker-compose.yml сервис web и удалим проброс порта 5000 для productservice.
services: productservice: build: product-service web: build: nginx ports: - "80:80"
Как видите, порт хоста 80 пробрасывается на порт контейнера 80
В структуру проекта добавим директорию nginx с файлами Dockerfile и nginx.conf
wired-brain ├── docker-compose.yml ├── nginx │ ├── Dockerfile │ └── nginx.conf └── product-service ├── Dockerfile ├── requirements.txt ├── src │ └── app.py └── venv ├── bin ├── lib ├── lib64 -> lib └── pyvenv.cfg
Dockerfile
FROM nginx COPY nginx.conf /etc/nginx/nginx.conf
nginx.conf
events { } http { server { listen 80; # Simple reverse-proxy # Pass requests for dynamic content to the Flask server location / { proxy_pass http://productservice:5000/; } } }
Здесь порт контейнера nginx 80 (на который идёт проброс извне) перебрасывается на
порт 5000 контейнера productservice, на котором слушает Flask.
Docker Compose в состоянии сопоставить адрес productservice с текущим
IP адресом
контейнера в докер сети
Раньше: Host:5000 -> productservice:5000 Сейчас: Host:80 -> nginx:80 -> productservice:5000