Привет, %username%
! Поскольку Docker мы уже установили, теперь надо что-то в нем запустить. И как только мы захотели что-то запустить, то первое с чем мы сталкиваемся это Dockerfile
и docker-compose.yml
. О них и будет речь далее.
Образы Docker
Для начала заглянув в документацию вспомним, что же такое контейнер Docker и поймем, что Docker-контейнер - это Docker image (образ) который оживили. Собственно говоря Docker image - это то, из чего запускается любой Docker-container.
Каждому Docker image соответствует свой набор инструкций и файл с такими инструкция называется Dockerfile
– без расширений и каких-либо точек. Из Dockerfile
в дальнейшем собираются Docker images (образы). Сброка нового образа выполняется запуском команды:
docker build -f ./Dockerfile
Учитывая, что наш Dockerfile расположен в той же директории где и мы запускаем команду сборки, то команду можно упростить до такого вида:
docker buid
Каждый контейнер состоит из слоев, а каждый слой контейнера (за исключением последнего) предназначен исключительно для чтения. Собственно основная задача Dockerfile состоит в том, чтобы описать последовательность данных слоев – в каком порядке они идут.
Учитывая, что в Unix все есть файл мы можем сказать, что отдельный слой это всего лишь файл, который описывает изменение состояния по сравнению с предыдущим слоем.
Базовый образ является лишь исходным слоем (слоями) создаваемого образа (иногда называют родительским образом).
Файл Dockerfile
В Dockerfile написаны инструкции по соданию образа. Каждая инструкция пишется с начала строки заглавными буквами, а после инструкции идут ее аргументы. Обработка инструкций происходит сверху вниз согласно тому порядку, как они написаны в файле. Простейший пример Dockerfile выглядит следующим образом:
FROM ubuntu:20.04
COPY . /var/www/html
Новые слои в итоговом образе создаются только инструкциями FROM
, RUN
, COPY
, ADD
. Остальные инструкции что-то описывают, настраивают или общаются с Docker’ом говоря, например – открыть такой-то порт.
Инструкции Dockerfile
FROM
— задаёт базовый (родительский) образ.LABEL
— описывает метаданные. Например — сведения о том, кто создал и поддерживает образ.ENV
— устанавливает постоянные переменные среды.RUN
— выполняет команду и создаёт слой образа. Используется для установки в контейнер пакетов.COPY
— копирует в контейнер файлы и папки которые лежат локально.ADD
— копирует файлы и папки в контейнер, может распаковывать локальные .tar-файлы, а так же получать на вход URL и скачивать файл внутрь image.CMD
— описывает команду с аргументами, которую нужно выполнить когда контейнер будет запущен. Аргументы могут быть переопределены при запуске контейнера. В файле может присутствовать лишь одна инструкцияCMD
.WORKDIR
— задаёт рабочую директорию для следующей инструкции.ARG
— задаёт переменные для передачи Docker во время сборки образа.ENTRYPOINT
— предоставляет команду с аргументами для вызова во время выполнения контейнера. Аргументы не переопределяются.EXPOSE
— указывает на необходимость открыть порт. Также можно открыть socket, но это тема для отдельной заметки.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!
Если у тебя есть вопросы, комментарии и/или замечания – заходи в чат, а так же подписывайся на канал.