В этой статье речь пойдет о том, как я тестирую свой python код внутри docker контейнера с Alpine Linux. Для тех, кто не знает, что такое docker, поясню, что это инструмент для управления контейнерами, который использует «слоёные» файловые системы и написан на Go.
Для меня основное преимущество docker перед привычной виртуализацией, например Qemu/KVM, в его легковесности. За счет AuFS/OverlayFS базовый образ системы и контейнер занимают до нескольких сотен мегабайт, а новые контейнеры используют тот же образ, что и старые, что позволяет экономить еще больше места. Docker есть в репозиториях многих дистров, так что вы его сможете легко поставить.
Что касается моего приложения, то оно написано на Python и использует Flask. Оно пока в активной разработке и выложить исходники я не могу. Приложение реализует RESTful API для gdnsd. Gdnsd — это авторитативный DNS сервер, который умеет балансировку и геораспределение. Сначала я тестировал со стандартным, встроенным во Flask web сервером, но после добавления в приложение поддержки uwsgi, я решил тестировать с uwsgi. Первым делом, в голову пришло поднять виртуалку и тестить в ней, но это показалось слишком громоздким, поэтому я засунул все внутрь docker.
В качестве базовой системы был выбран Alpine Linux за его крошечный размер — 5 МБ. Я был приятно удивлен, когда обнаружил все необходимые мне пакеты под Alpine.
Вот мой Dockerfile, который расположен прямо в директории приложения
FROM alpine
RUN apk add --update python uwsgi-python py-pip mercurial gdnsd && \
rm -rf /var/cache/apk/*
COPY . /opt/gdnsd-api
RUN pip install -r /opt/gdnsd-api/requirements.txt
RUN hg init /etc/gdnsd && \
mkdir -p /opt/config/example && \
cp /opt/gdnsd-api/wsgi/api.ini /opt/config/ && \
sed -i 's|pyargv.*|pyargv=/opt/config/api.json|' /opt/config/api.ini && \
cp /opt/gdnsd-api/example/api.json /opt/config/ && \
sed -i 's|example|/opt/config/example|' /opt/config/api.json && \
cp /opt/gdnsd-api/example/config.json /opt/config/example && \
hg init /opt/config/example
EXPOSE 5000
ENTRYPOINT ["uwsgi", "--ini", "/opt/config/api.ini", "--plugin-dir", \
"/usr/lib/uwsgi", "--plugin", "python", "--protocol", "http", \
"--socket", "0.0.0.0:5000", "--py-autoreload", "1"]
FROM alpine
из какого образа создавать docker imageRUN apk add ...
устанавливает нужные для работы приложения пакетыCOPY . /opt/gdnsd-api
копируем код приложения внутрь imageRUN pip install -r /opt/gdnsd-api/requirements.txt
устанавливает все зависимости приложения- дальше мы копируем примеры конфигов и правим пути в скопированных конфигах
EXPOSE 5000
говорит демону docker, о том, какие порты слушает контейнерENTRYPOINT
команда, которая будет выполняться в контейнере, запущенном из построенного image
Рассмотрим ENTRYPOINT подробнее. Я запускаю uwsgi с модифицированным ранее конфигом /opt/config/api.ini, говорю, что нужно искать плагины в /usr/lib/uwsgi и подгружать оттуда python плагин, потому что это не делается автоматически. Используем протокол http, чтоб не устанавливать еще и nginx. Слушаем порт 5000 на всех интерфейсах и перезагружаем приложение при изменении кода - py-autoreload.
Вот сам api.ini для наглядности
[uwsgi]
module = app
callable = app
master = true
processes = 1
socket = 127.0.0.1:8000
chdir = /opt/gdnsd-api
wsgi-file = app.py
uid = root
gid = root
pyargv = /opt/config/example/api.json
buffer-size = 32768
Когда мы вдумчиво написали Dockerfile, можем создать image
docker build -t myapp .
Создание моего image, после выкачки базового образа alpine и установки пакетов занимает около 15 секунд.
Образ готов и весит всего 73МБ вместе с приложением и его зависимостями, а не 188 как чистая Ubuntu.
docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
myapp latest 410d923182b5 5 hours ago 72.68 MB
ubuntu 14.04 36248ae4a9ac 4 days ago 187.9 MB
alpine latest 3571dd565f47 4 days ago 4.79 MB
Теперь запустим контейнер из свежесозданного образа
docker run -v /my-app:/opt/gdnsd-api:ro -p 127.0.0.1:5000:5000 \
--name myapp myapp
С помощью -v (volume) мы прокинем код нашего приложения из хост системы в контейнер, при чем в режиме только для чтения (:ro). Т.к. разработку я веду на хосте (в ОС ноутбука), а не в контейнере, то мне удобней, когда контейнер получает изменения сразу после сохранения файла на хосте. Деплоить приложение внутрь контейнера после каждого изменения слишком накладно для тестирования, а для распростраения приложения — в самый раз. Подробнее про docker volumes вы можете почитать вот здесь. -p редиректит порт 5000 из контенера в хост систему.
Получить shell в запущенном контейнере можно вот так:
docker exec -ti myapp sh
Тестирую приложение я локально в Postman, очень удобное приложение, рекомендую.