Docker, в последнее время стал всеобщим увлечением, – это удивительный, мощный инструмент для упаковки, отправки и запуска приложений. Однако на то, чтобы понять схему работы и подготовить Docker для конкретного приложения, может уйти достаточно времени. Так как в интернете полно гайдов о концепции контейнеров, я не буду слишком много рассуждать о нем. Вместо этого я объясню, что значит каждая написанная строчка и как можно применить эти знания к конкретному приложению или конфигурации.
Почему Docker?
Я являюсь частью управляемой студентами некоммерческой организации под названием Hack4Impact в Иллинойском университете в Урбане-Шампейне. Мы разрабатываем технические проекты для некоммерческих организаций, чтобы помочь им в выполнении их задач. Каждый семестр в нашем распоряжении находится несколько проектных команд из 5-7 студентов-разработчиков ПО разного уровня квалификации. Есть даже студенты, которые только закончили первый курс информатики в колледже.
Поскольку многие некоммерческие организации часто запрашивали веб-приложения, я курировал Flask Boilerplate, чтобы позволить командам быстро запускать их серверные службы REST API. Предоставлялись общие функции утилит, структура приложения, оболочки базы данных и соединения, а также документация по настройке, лучшие практики кодирования и действия по развертыванию Heroku.
Проблемы со средой разработки и зависимостями
Так как мы каждый семестр принимаем на работу новых студентов-разработчиков ПО, команды тратят много времени на настройку и устранение проблем среды. У нас часто было несколько участников, работающих в разных операционных системах. Мы сталкивались с множеством проблем (Windows, я указываю на вас). Хотя многие из этих проблем были тривиальными (например, запуск правильной версии БД PostgreSQL с правильным именем пользователя/паролем), это трата времени, которое могло быть вложено в сам продукт.
Кроме того, я написал документацию только для пользователей Mac OS с инструкциями по bash (у меня есть Mac). По сути, я оставил пользователей Windows и Linux без внимания. Я мог бы запустить несколько виртуальных машин и снова задокументировать настройки для каждой ОС, но зачем мне это делать, если есть Docker?
Пора воспользоваться Docker
С помощью Docker все приложение можно изолировать в контейнерах, которые можно переносить с компьютера на компьютер. Благодаря этому не возникает конфликтов между средами и зависимостями. Таким образом, вы можете «собрать один раз, запустить где угодно». Разработчики теперь могут установить только одну вещь – Docker – и ввести пару команд для запуска приложения. Новички смогут быстро начать развиваться, не беспокоясь о среде. В будущем некоммерческие организации также смогут быстро вносить изменения.
Docker также имеет много других преимуществ, таких как портативность и ресурсоэффективность (по сравнению с виртуальными машинами). Важно и то, что вы можете безболезненно настроить непрерывную интеграцию и быстро развернуть свое приложение.
Краткое описание основных компонентов Docker
В интернете есть много ресурсов, которые объяснят Docker лучше, чем я, поэтому я не буду подробно останавливаться на них. Я рассмотрю некоторые из основных компонентов Docker, которые необходимы для понимания остальной части этой статьи.
Образы Docker
Образы Docker – это шаблоны, доступные только для чтения. Они описывают контейнер Docker. Они включают в себя конкретные инструкции, написанные в Dockerfile. Он определяет приложение и его зависимости. Можно представить, что это снимок вашего приложения в определенное время. Образы можно получить при введении команды docker build
.
Контейнеры Docker
Контейнеры Docker являются экземплярами образов Docker. Они включают в себя ОС, код приложения, среду, системные инструменты, библиотеки и т. д. Можно объединить несколько Docker-контейнеров. Например, приложение Node.js может находиться в контейнере, который связан с контейнером БД Redis. Чтобы запустить контейнер Docker, нужно воспользоваться командой docker start
.
Реестры Docker
Реестр Docker – это место, где можно хранить и распространять образы. Мы будем использовать базовые образы из DockerHub - бесплатного реестра, размещенного самим Docker.
Docker Compose
Docker Compose – это инструмент, который позволяет создавать и запускать несколько образов Docker одновременно. Вместо того, чтобы каждый раз проводить одни и те же команды, когда вы хотите запустить свое приложение, можно уместить их в одну – как только вы предоставите конкретную конфигурацию.
Пример Docker с Flask и Postgres
Держа в уме все компоненты, давайте приступим к настройке среды разработки Docker с использованием приложения Flask и Postgres в качестве хранилища данных. В оставшейся части этого поста я буду ссылаться на Flask Boilerplate – репозиторий, который я упоминал ранее, говоря о Hack4Impact.
В этой конфигурации мы создадим в Docker 2 образа:
- App – приложение Flask, которое находится на порте 5000;
- Postgres – база данных Postgres, которая находится на порте 5432.
Что касается верхнего каталога, эту конфигурацию определяет 3 файла:
Dockerfile – скрипт, составленный из инструкций по настройке контейнеров приложения. Каждая команда является автоматической и выполняется последовательно. Этот файл будет находиться в каталоге, в котором вы запускаете приложение (например, python manage.py runserver
, python app.py
или npm start
). В нашем случае он находится в верхнем каталоге (где расположен manage.py
). Dockerfile принимает инструкции Docker;
.dockerignore указывает, какие файлы не включать в контейнер. Работает так же, как и .gitignore
, но в контейнерах Docker. Этот файл связан с Dockerfile;
docker-compose.yml – файл конфигурации для Docker Compose. Он позволит нам одновременно создавать образы app
и postgres
, определять объемы и состояния, в которых приложение зависит от postgres
, и устанавливать необходимые переменные среды.
На 2 образа приходится один Dockerfile, потому что мы будем брать официальный образ Docker Postgres из DockerHub! Вы можете добавить свой собственный образ Postgres, написав для него отдельный Dockerfile, но в этом нет никакого смысла.
Dockerfile
Проясним еще раз: этот Dockerfile предназначен для контейнера приложения. В качестве обзора приведен весь Dockerfile – он получает базовый образ, копирует приложение, устанавливает зависимости и устанавливает конкретную переменную среды.
FROM python:3.6
LABEL maintainer "Timothy Ko <tk2@illinois.edu>"
RUN apt-get update
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV="docker"
EXPOSE 5000
Поскольку это приложение Flask использует Python 3.6, мы хотим, чтобы среда поддерживала его, так что он должен быть установлен в ней. К счастью, DockerHub имеет официальный образ, установленный поверх Ubuntu. В одной строке у нас будет базовый образ Ubuntu с Python 3.6, virtualenv и pip. В DockerHub есть множество образов. Если вы хотите начать с нового образа Ubuntu и «строить» поверх него, вы можете сделать это.
FROM python:3.6
Затем я отмечаю, что я являюсь специалистом по обслуживанию.
LABEL maintainer "Timothy Ko <tk2@illinois.edu>"
Теперь пришло время добавить приложение Flask к образу. Для простоты я решил скопировать приложение в каталог /app
в нашем образе Docker.
RUN mkdir /app
COPY . /app
WORKDIR /app
WORKDIR
– это, по сути, cd
. COPY
копирует определенный каталог в предоставленный каталог в образе. ADD
- это еще одна команда, которая делает то же самое, что и COPY
, но она также позволяет вам добавлять каталоги из URL. Таким образом, если вы хотите клонировать свой git-репозиторий, а не копировать его из локального каталога (для промежуточных и производственных целей), вы можете использовать такой путь. Однако обычно COPY
следует использовать, если у вас нет URL. Каждый раз, когда вы используете RUN
, COPY
, FROM
или CMD
, вы создаете новый слой в образе Docker, который влияет на способ хранения и кэширования образов в Docker.
Раз уж мы скопировали каталог в образ, установим все наши зависимости, которые определены в файле requirements.txt
.
Но, скажем, у вас было приложение Node, а не Flask. Тогда вы бы писали RUN npm install
. Следующим шагом является указание Flask использовать конфигурации Docker, которые я жестко запрограммировал в config.py
. В этой конфигурации Flask подключится к правильной базе данных, которую мы настроим позже. Поскольку у меня были производственные и обычные конфигурации разработки, я сделал так, чтобы Flask выбирал конфигурацию Docker всякий раз, когда для переменной среды FLASK_ENV задано значение docker. Итак, нам нужно установить это в образе нашего приложения.
ENV FLASK_ENV="docker"
Затем откроем порт 5000, на котором работает приложение Flask:
EXPOSE 5000
Вот и все! На какой бы ОС вы ни работали, как бы плохо вы ни выполняли инструкции, ваш образ Docker будет таким же, как и у других членов команды. Все благодаря этому Dockerfile.
Каждый раз, когда вы будете создавать образ, будут проводиться упомянутые команды. Вы можете построить образ с помощью sudo docker build -t app
. Однако, если запускать контейнер с помощью sudo docker run app
, в приложении возникнет ошибка по подключению к БД. Все потому, что мы еще не ввели в действие саму базу данных.
docker-compose.yml
Docker Compose позволяет одновременно делать так и одновременно создавать образ app
. Весь файл выглядит так:
version: '2.1'
services:
postgres:
restart: always
image: postgres:10
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- ./postgres-data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
app:
restart: always
build: .
ports:
- 5000:5000
volumes:
- .:/app
Для этого каталога я решил использовать версию 2.1, так как мне было удобнее с ней, да и на нее больше туториалов. Да, это моя единственная причина не использовать версию 3. В версии 2 вы должны предоставить «сервисы» или изображения, которые вы хотите включить. В нашем случае это app
и postgres
(это просто имена, на которые вы можете ссылаться, когда используете команды docker-compose
. Их можно называть БД, api или чем-то еще).
Образ Postgres
Что касается Postgres Service, я указываю, что это образ postgres:10
, которое является еще одним образом из DockerHub. Этот образ является образом Ubuntu, на котором установлен Postgres, и он автоматически запускает сервер Postgres.
postgres:
restart: always
image: postgres:10
environment:
- POSTGRES_USER=${USER}
- POSTGRES_PASSWORD=${PASSWORD}
- POSTGRES_DB=${DB}
volumes:
- ./postgres-data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
Если вы хотите применить другую версию, просто замените «10» на что-то другое. Чтобы указать, какого пользователя, пароль и базу данных вы хотите использовать в Postgres, нужно заранее определить переменные среды. Это реализовано в официальном Dockerfile образа для Postgres. В этом случае образ postgres внедрит переменные среды $USER
, $PASSWORD
и $DB
и сделает их переменными среды - POSTGRES_USER
, POSTGRES_PASSWORD
и POSTGRES_DB
внутри контейнера postgres. Обратите внимание, что $USER
и другие введенные переменные являются переменными среды, указанными на вашем собственном компьютере (точнее – это процесс командной строки, который вы используете для запуска команды docker-compose up
). Введение учетных данных позволяет вам не передавать их в публичный каталог.
Docker Compose также автоматически внедрит переменные среды, если в том же каталоге, где есть файл docker-compose.yml
, находится файл .env
. Вот пример файла .env
для такого случая:
USER=testusr
PASSWORD=password
DB=testdb
Таким образом, наша БД PostgreSQL будет называться testdb с пользователем testusr и с паролем password.
Наше приложение Flask будет подключаться к этой конкретной БД, потому что я записал его URL в конфигурации Docker, о чем я упоминал ранее.
Каждый раз, когда работа контейнера останавливается, а он сам удаляется, то также будут удаляться и данные. Значит, вы должны обеспечить постоянное хранилище данных, чтобы данные не удалялись. Есть 2 способа это сделать:
- Docker Volumes;
- Локальные каталоги.
Я решил подключить приложение локально к ./postgres-data/postgres
, но местоположение может быть любым. Синтаксис всегда такой: [ХОСТ]:[КОНТЕЙНЕР]
. Это означает, что любые данные из /var/lib/postgresql/data
на самом деле хранятся в ./postgres-data
.
volumes:
- ./postgres-data/postgres:/var/lib/postgresql/data
Тот же синтаксис используется и для портов.
ports:
- "5432:5432"
Образ app
Теперь определим образ app.
app:
restart: always
build: .
ports:
- 5000:5000
volumes:
- .:/app
depends_on:
- postgres
entrypoint: ["python", "manage.py","runserver"]
Сначала сделаем так, чтобы в нем был restart: always
. Это значит, что в случае ошибки приложение будет перезапускаться. Эта функция особенно удобна при создании и запуске контейнеров. Обычно app
запускается перед postgres
. Значит, app попытается подключиться к БД и выдаст ошибку, ведь postgres
еще не запущен. Без использованного нами свойства app
бы просто перестал работать, и на этом все бы кончилось.
Затем мы определяем, что этот билд должен быть Dockerfile в текущем каталоге.
build: .
Следующий шаг важен для того, чтобы сервер Flask перезапускался при изменении кода в локальном каталоге. Это очень полезно, ведь вам не придется каждый раз выпускать новые версии образа, чтобы увидеть изменения. Чтобы добиться этого, делаем то же, что и ранее с postgres. Устанавливаем, что в каталоге /app
в контейнере будет находиться все, что есть в .
(в текущем каталоге). Благодаря этому все изменения в локальном репозитории будут перенесены в контейнер.
volumes:
- .:/app
Затем сообщаем Docker Compose, что приложение зависит от контейнера postgres
. Обратите внимание, что при изменении названия образа на что-то вроде database
нужно заменить на него postgres
.
depends_on:
- postgres
Наконец, предоставим команду, которая вызывается для запуска приложения. В нашем случае это python manage.py runserver
.
entrypoint: ["python", "manage.py","runserver"]
В случае Flask нужно точно сообщить, какой хост (порт) используется для приложения. Также определите, нужно ли, чтобы оно было в отладочном режиме при запуске. В manage.py
я делаю это так:
def runserver():
app.run(debug=True, host=’0.0.0.0', port=5000)
Теперь нужно задать и запустить приложение Flask и БД Postgress с помощью командной строки:
docker-compose build
docker-compose up -d
docker-compose exec app python manage.py recreate_db
По сути, последняя команда создает схему БД, которая определяется приложением Flask в Postgres.
Вот и все! Ваше приложение Flask теперь должно запускаться по ссылке http://localhost:5000
!
Команды Docker
По началу может быть сложно запоминать и находить команды для Docker, так что по ссылке можно найти их список.
Заключение
Docker действительно позволяет командам вести разработки гораздо быстрее. Это возможно благодаря портативности и одинаковых средах на всех платформах. Несмотря на то что я использовал Docker только в разработках, он также ускоряет работу при непрерывной интеграции, тестировании и внедрении приложения.
Можно было бы добавить еще пару строчек, и тогда бы получилась полная версия приложения с Nginx и Gunicorn. Если бы я захотел использовать Redis для кэширования сессии или для создания очереди, я бы мог сделать это очень быстро. У всей моей команды была бы та же среда разработки, что и у меня, если бы они использовали эти образы Docker.
Кстати, при желании я бы мог даже моментально сделать 20 версий приложения Flask. Спасибо за чтение!