[Docker] Основы Docker: Dockerfile и docker-compose.yml

Привет, %username%! Поскольку Docker мы уже установили, теперь надо что-то в нем запустить. И как только мы захотели что-то запустить, то первое с чем мы сталкиваемся это Dockerfile и docker-compose.yml. О них и будет речь далее.

Образы Docker

Для начала заглянув в документацию вспомним, что же такое контейнер Docker и поймем, что Docker-контейнер — это Docker image (образ) который оживили. Собственно говоря Docker image — это то, из чего запускается любой Docker-container.

Каждому Docker image соответствует свой набор инструкций и файл с такими инструкция называется Dockerfile – без расширений и каких-либо точек. Из Dockerfile в дальнейшем собираются Docker images (образы). Сброка нового образа выполняется запуском команды:

docker build

Файл Dockerfile должен быть размещен в той же директории, где мы выполняем команду.

Каждый контейнер состоит из слоев, а каждый слой контейнера (за исключением последнего) предназначен исключительно для чтения. Собственно основная задача Dockerfile состоит в том, чтобы описать последовательность данных слоев – в каком порядке они идут.

Учитывая, что в Unix все есть файл мы можем сказать, что отдельный слой это всего лишь файл, который описывает изменение состояния по сравнению с предыдущим слоем.

Базовый образ является лишь исходным слоем (слоями) создаваемого образа (иногда называют родительским образом).

Файл Dockerfile

В Dockerfile написаны инструкции по соданию образа. Каждая инструкция пишется с начала строки заглавными буквами, а после инструкции идут ее аргументы. Обработка инструкций происходит сверху вниз согласно тому порядку, как они написаны в файле. Простейший пример Dockerfile выглядит следующим образом:

FROM ubuntu:20.04
COPY . /var/www/html

Новые слои в итоговом образе создаются только инструкциями FROM, RUN, COPY, ADD. Остальные инструкции что-то описывают, настраивают или общаются с Docker’ом говоря, например – открыть такой-то порт.

Инструкции Dockerfile

  1. FROM — задаёт базовый (родительский) образ.
  2. LABEL — описывает метаданные. Например — сведения о том, кто создал и поддерживает образ.
  3. ENV — устанавливает постоянные переменные среды.
  4. RUN — выполняет команду и создаёт слой образа. Используется для установки в контейнер пакетов.
  5. COPY — копирует в контейнер файлы и папки которые лежат локально.
  6. ADD — копирует файлы и папки в контейнер, может распаковывать локальные .tar-файлы, а так же получать на вход URL и скачивать файл внутрь image.
  7. CMD — описывает команду с аргументами, которую нужно выполнить когда контейнер будет запущен. Аргументы могут быть переопределены при запуске контейнера. В файле может присутствовать лишь одна инструкция CMD.
  8. WORKDIR — задаёт рабочую директорию для следующей инструкции.
  9. ARG — задаёт переменные для передачи Docker во время сборки образа.
  10. ENTRYPOINT — предоставляет команду с аргументами для вызова во время выполнения контейнера. Аргументы не переопределяются.
  11. EXPOSE — указывает на необходимость открыть порт. Также можно открыть socket, но это тема для отдельной заметки.
  12. VOLUME — создаёт точку монтирования для работы с постоянным хранилищем.

Подробности и нюансы можно почерпнуть из официальной документации.

Живой пример Dockerfile

Вот живой, но довольно простой пример Dockerfile:

# В качестве родителя используем Python v3.8 основанный на Ubuntu
FROM python:3.8

# Просим Python не писать .pyc файлы
ENV PYTHONDONTWRITEBYTECODE 1

# Просим Python не буферизовать stdin/stdout
ENV PYTHONUNBUFFERED 1

# Задаем рабочую директорию
WORKDIR /opt/app

# Копируем файл с зависимостями
COPY ./req.txt /opt/app/requirements.txt

# Устанавливаем зависимости
RUN pip install -r /opt/app/requirements.txt

# Копируем исходный код приложения
COPY ./src /opt/app

# Говорим что надо открыть снаружи порт 8000
EXPOSE 8000

# Команда которая должна быть выполнена при запуске контейнера
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Думаю ничего сложного тут нет, учитывая все комментарии и вышеизложенное.

Сборка/публикация в registry

Вот мы обзавелись простым Dockerfile, а что дальше? Дальше нам необходимо выполнить сборку:

docker build -t jtprog/django_movie:0.1 .

По порядку: мы говорим докеру сбилдить образ, дать ему имя django_movie и тэг (-t – сокращение от —tag) 0.1, ну а Dockerfile взять из текущей директории (точка в конце). jtprog – говорит о том, что данный образ будет принадлежать мне и будет привязан к моему аккаунту на hub.docker.com

Теперь у нас есть собраный образ и он хранится локально. Посмотреть его можно вот так:

docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
jtprog/django_movie   0.1                 e4d1fb505769        24 minutes ago      1.04GB
python                3.8                 4e2d08f34f6d        3 days ago          934MB

Данная команда покажет все images которые у нас есть и локально доступны – скачаны. Собственно после сборки образа его можно отправить в реестр (docker registry), дабы им могли воспользоваться другие или вы сами с другого компьютера/сервера. Самым простым вариантом в таком случае является собственно Docker Hub и к тому же он бесплатный. Регистрация там простая, так что справитесь.

После регистрации нам необходимо выполнить в консоли вот эту команду:

docker login

У вас будет спрошен адрес удаленного реестра, логин и пароль для доступа к нему. Указываете свои логин и пароль – готово!

Теперь отправим свой образ в реестр – на Docker Hub. Делается это так же довольно просто:

docker image push jtprog/django_movie:0.1

Теперь наш Docker image доступен всем и каждому вот тут – django_movie.

Запуск

Научились простенько собирать образы и публиковать их на Docker Hub, а теперь попробуем воспользоваться нашим образом. Попробуем запустить из него контейнер. Для этого выполним вот такую команду:

docker run -p 8000:8000 --detach --name movie jtprog/django_movie:0.1

Тут по порядку: docker run – просим запустить, -p 8000:8000 – пробросить порт 8000 с нашего сервера (компуктера) на порт 8000 нашего контейнера, --detach – отключиться (не интерактивная работа с контейнером), --name movie – называем наш контейнер movie, jtprog/django_movie:0.1 – имя пользователя в реестре/имя образа/тэг. Вроде внятно и понятно.

Теперь в списке запущенных контейнеров наш свеженький появится с именем movie. А посмотреть список запущенных контейнеров можно следующей командой:

docker ps

И вроде бы всё хорошо, мы видим наш контейнер, но что-то мы забыли – не кажется? Правильно – у нас же ж простое приложение на Django, которое использует в качестве БД PostgreSQL. А мы ничего для ее – БД – запуска не сделали, соответственно наше приложение не может подключиться для инициализации к базе данных.

Используем docker-compose

Согласно одной из легенд, docker-compose появился после того, как к разработчикам Docker пришли и сказали: “Docker – отличная вещь! Но сделайте удобно!”. Будем считать что он уже поставлен у вас. Так что сразу перейдем к делу – содаем локально рядом с Dockerfile еще и docker-compose.yml.

И приводим его к такому виду:

# Версия Docker API
version: '3.7'
# Сервисы которые мы будем запускать
services:
# Первый сервис - db
  db:
    # Образ на основе которого он будет запускаться
    image: postgres:12-alpine
    # volumes - магическая вещь, которая создает некоторое устройство в 
    # рамках Docker и монтирует его в директорию /var/lib/postgresql/data
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    # Переменные окружения 
    environment:
      POSTGRES_USER: movie
      POSTGRES_PASSWORD: 123456
      POSTGRES_DB: movie
    # Говорим открыть снаружи порт 5432
    expose:
      - 5432

    # Второй сервис - app
  app:
    # Говорим что его надо будет собрать - в качестве контекста
    # передаем текущую директорию - в ней лежит Dockerfile
    build: .
    # Монтируем локальную директорию ./src в директорию 
    # внутри контейнера /opt/app
    volumes:
      - ./src:/opt/app
    # Говорим пробросить порт 8000 хоста в порт 8000 контейнера
    ports:
      - 8000:8000
    # Зависит от сервиса db - запускать после него
    depends_on:
      - db
# Просто говорим создать volume с именем postgres_data
volumes:
  postgres_data:

После этого в консоли выполним вот эту команду:

docker-compose up --build -d

Тут мы говорим: up – поднять, --build – собрать, -d – пусть робит в фоне. После чего мы можем посмотреть список запущенных сервисов:

docker-compose ps
         Name                       Command               State           Ports
----------------------------------------------------------------------------------------
django_movie_drf_app_1   python /opt/app/manage.py  ...   Up      0.0.0.0:8000->8000/tcp
django_movie_drf_db_1    docker-entrypoint.sh postgres    Up      5432/tcp

Важно! Все команды docker-compose должны выполняться в той же директории где расположен файл docker-compose.yml. В противном случае необходимо его указывать явно через флаг -f и путь до файла docker-compose.yml.

К слову у вас может быть несколько файлов docker-compose.yml и их можно включать все например вот такой конструкцией:

docker-compose -f docker-compose.yml -f docker-compose.admin.yml run -it backup_db

Кейс: в файле docker-compose.admin.yml может быть больше доступа. Или для среды разработки можно поднимать моки вместо реальных сервисов.

Так же замечу, что все сервисы описанные в рамках одного docker-compose.yml файла (в нашем примере это сервисы db и app), будут сразу "из коробки" видеть друг друга по указанным именам.

Ну а теперь откройте в браузере http://127.0.0.1:8000/swagger/ и убедитесь, что приложение поднялось и заработало – откроется Swagger-документация по API нашего приложения. По крайней мере так может казаться на первый взгляд.

Работа напильником

Действительно, на первый взгляд может показаться, что все работает, но нет. Мы же в консоли через manage.py создаем миграции, накатываем их, собираем статику, создаем суперпользователя. Так что давай-ка подумаем: как нам выполнить всё то же самое в контейнере? И готов поспорить, что первое что пришло тебе в голову:

RUN python manage.py makemigrations 
RUN python manage.py migrate

Как только тебе это пришло в голову – иди попей чаю, покушай, отдохни, поспи! Короче гони из головы эту дичь! Я какбэ не запрещаю тебе страдать хернёй, но тем не менее делать так не советую.

Как быть дальше и с какой стороны/силы приложить напильник, будет описано в следующей статье.

На этом всё! Profit!