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

Похожие статьи
Flask
Основы
Python
Запуск Flask на хостинге
Запуск Flask на Linux сервере
Flask в Docker
Первый проект на Flask
Шаблоны Jinja
Web Forms
Blueprint - Чертежи Flask
Как разбить приложение Flask на части
Flask FAQ
Ошибки
Декораторы в Python
HTML
CSS

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: