Docker - дистрибуция приложений

Часто бывает такое, что нужен к примеру самый новый PHP или другой софт на сервере, плюсом требуется некоторая кастомизация системы и вы хотите чтобы ваше приложение заработало на другом сервере, у другого человека, чтобы он просто развернул контейнер и всё. Иногда нужны пропатченные библиотеки, а на другом компе это может поломать другие приложения.

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

Сегодня попробуем немного поэкспериментировать в Docker!

Все опыты будут выполняться на Debian 10, актуально и для убунты и для всего остального...

Для начала установим докер

apt install docker

В докере есть образы, а есть контейнеры, контейнер это так сказать экземпляр системы из образа, т.е. контейнер использует образ.

Если нужного для контейнера образа нет в системе, то он сам скачается...

Для большинства задач достаточно стандартных образов, этот блог на таком же и развернут, была статья раннее.

Но иногда нужно что-то особенное, для этого я решил углубить свои познания.

Самый минимальный образ это alpine. Но иногда стабильнее будет работать к примеру Debian или Ubuntu, т.к. в Alpine нет glibc.

Скачать образ можно так

docker pull ubuntu

А посмотреть список образов в системе вот так

docker images

Удалить образ можно так

docker rmi ubuntu

Создадим контейнер, в который сразу провалимся, команда run создает контейнер и выполняет

docker run -it ubuntu bash

По умолчанию у нас внутри нет ни nano, ни vim и даже apt install не работает, нужно выполнить например apt-get update но только в докере такая ситуация, как только вы выйдите из консоли и по новой запустите контейнер, все пропадет...

Чтобы хоть что-то сохранять, нужно или образ свой лепить на основе этого со своими модификациями, или пробрасывать свою папку внутрь контейнера, то что будет писаться в виртуальной среде будет и в папке...

docker ps - выводит запущенные контейнеры, а docker ps --all все, в том числе и остановленные

Если мы в команде run не указали никаких параметров, то докер сам придумывает имена контейнерам, я пару раз запустил баш в убунте и у меня два контейнера

root@debian-testing:~# docker ps --all
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
f877b8cfec0d        ubuntu              "bash"              5 minutes ago       Exited (0) 4 minutes ago                       frost
y_engelbart
543a77036c96        ubuntu              "bash"              9 minutes ago       Exited (0) 5 minutes ago                       nosta
lgic_shannon

Попробуем не создавать теперь, а просто снова запустить созданный контейнер

Если бы там был демон, то можно было сделать например

docker start f877b8cfec0d

Но у нас там баш, остается наверно только снести контейнеры с пустым башем

docker rm f877b8cfec0d 543a77036c96

Если нам нужно выполнить команду в определенном образе, но не хранить этот контейнер, он у нас не демон, то можно вот так, на пример php-cli

docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:7.4-cli php your-script.php

-v  - это прокинуть текущую папку в виртуальную внутрь контейнера /usr/src/myapp

-w - сразу перейти в эту директорию

Мне захотелось слепить свой образ, который будет компактным, ничего лишнего и с моими настройками, php.ini, еще чтобы там изначально были некоторые библиотеки, как например клиент для RabbinMQ, небольшие патчи локализации и некоторые вещи, которые я настраиваю в приложении, хотелось бы настроить в php.ini... Плюсом ко всему, приложение будет заперто от всех контейнеров. Хочу сделать сразу cli версию для фоновых обработчиков и fpm, а на хостовом компе будет nginx все это проксировать.

Попробуем создать свой образ, для начала возьмем то что есть в официальном образе, немного перелопатим и попробуем...

FROM alpine:3.12
RUN apk add --no-cache ca-certificates curl tar xz openssl

RUN set -eux; addgroup -g 82 -S www-data; adduser -u 82 -D -S -G www-data www-data
ENV PHP_INI_DIR /usr/local/etc/php

#создаем все папки для работы php
RUN set -eux; mkdir -p "$PHP_INI_DIR/conf.d"; \
[ ! -d /var/www/html ]; mkdir -p /var/www/html; \
chown www-data:www-data /var/www/html; chmod 777 /var/www/html

#создаем папку, качаем в нее файл
ENV PHP_URL="https://www.php.net/distributions/php-7.4.10.tar.xz"
RUN mkdir -p /usr/src/php; cd /usr/src; \
curl -fsSL -o php.tar.xz "$PHP_URL";
#распакуем
RUN tar -Jxvf /usr/src/php.tar.xz -C /usr/src/php --strip-components=1

#временно устанавливаем все необходимое для компиляции
RUN set -eux; apk add --no-cache --virtual .build-deps \
autoconf dpkg-dev dpkg file g++ gcc libc-dev make \
pkgconf re2c argon2-dev coreutils curl-dev \
libedit-dev libsodium-dev libxml2-dev linux-headers \
oniguruma-dev openssl-dev sqlite-dev

#компилируем
ENV PHP_CFLAGS="-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
ENV PHP_CPPFLAGS="$PHP_CFLAGS"
ENV PHP_LDFLAGS="-Wl,-O1 -pie"

RUN export CFLAGS="$PHP_CFLAGS" CPPFLAGS="$PHP_CPPFLAGS" \
LDFLAGS="$PHP_LDFLAGS"; \
cd /usr/src/php; \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
./configure \
--build="$gnuArch" \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \
--enable-option-checking=fatal \
--with-mhash --enable-ftp --enable-mbstring --enable-mysqlnd \
--with-password-argon2 --with-pdo-sqlite=/usr \
--with-sqlite3=/usr \
--with-curl --with-libedit --with-openssl --with-zlib \
--with-pear --enable-fpm --with-fpm-user=www-data \
--with-fpm-group=www-data \
--disable-cgi; \
make -j 4; \
find -type f -name '*.a' -delete; \
make install; \
find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; \
make clean; \
cp -v php.ini-* "$PHP_INI_DIR/"

RUN	cd /; \
runDeps="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local | tr ',' '\n' | sort -u \
| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }')"; \
	apk add --no-cache $runDeps; \
	\
	apk del --no-network .build-deps; \
	\
	pecl update-channels; \
	rm -rf /tmp/pear ~/.pearrc /usr/src/php;

RUN set -eux; \
cd /usr/local/etc; \
cp php-fpm.conf.default php-fpm.conf; \
cp php-fpm.d/www.conf.default php-fpm.d/www.conf; \
sed -i 's!=NONE/!=!g' php-fpm.conf;\
{ \
		echo '[global]'; \
		echo 'error_log = /proc/self/fd/2'; \
		echo 'log_limit = 8192'; \
		echo '[www]'; \
		echo 'access.log = /proc/self/fd/2'; \
		echo 'clear_env = no'; \
		echo 'catch_workers_output = yes'; \
		echo 'decorate_workers_output = no'; \
	} | tee php-fpm.d/docker.conf; \
	{ \
		echo '[global]'; \
		echo 'daemonize = no'; \
		echo '[www]'; \
		echo 'listen = 9000'; \
	} | tee php-fpm.d/zz-docker.conf

STOPSIGNAL SIGQUIT

EXPOSE 9000
CMD ["php-fpm"]
Dockerfile

Желательно создавать из пустой папки, положить в нее только Dockerfile

собираем образ php-mp (со своим префиксом, чтобы не пересекаться с официальными образами)

docker build -t php-mp .

Всё собралось, для php-fpm нет понятия document-root, он просто запускает тот файл, который передан из nginx, минимально тестово можно сделать так

...
root /var/www/html
location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass localhost:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
...

И запустить, предварительно положив в /var/www/html index.php с содержимым типа <?php phpinfo();

 docker run -d -p 9000:9000 -v /var/www/html:/var/www/html -w /var/www/html php-mp

Всё получилось...

А теперь самое интересное, посмотрим список образов

# docker images
REPOSITORY     TAG       IMAGE ID       CREATED             SIZE
php-mp       latest    5b8c7cf24f1e   58 minutes ago        477MB
php          7.4-cli   285d825479be4  7 days ago            405MB
ubuntu       latest    46e2eef94cd6b  3 weeks ago           73.9MB
alpine       3.12      a24bb4013296   3 months ago          5.57MB

Интересная картинка, я собирал на основе alpine, который 5 мегабайт, но у меня получился образ на 477 мегабайт, официальный php образ тоже не маленький, 405 мегабайт... В чем же дело?

Попробуем посмотреть историю изменения нашего образа

# docker history php-mp
IMAGE              CREATED BY                                      SIZE               
4b8c7cf24f1e           /bin/sh -c #(nop)  CMD ["php-fpm"]              0B
322041d76f21         /bin/sh -c #(nop)  EXPOSE 9000                  0B
1a6ac1349fb4         /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B
afc59ced57ba         /bin/sh -c set -eux; cd /usr/local/etc; cp p…   25.4kB
d4ba7843c37a       /bin/sh -c cd /; runDeps="$( scanelf --neede…   51.6kB
6786fe77ed30        /bin/sh -c export CFLAGS="$PHP_CFLAGS" CPPFL…   78.6MB
1104663835d0       /bin/sh -c #(nop)  ENV PHP_LDFLAGS=-Wl,-O1 -…   0B
7267e5e725a3       /bin/sh -c #(nop)  ENV PHP_CPPFLAGS=-fstack-…   0B
78e9c4a80b2a       /bin/sh -c #(nop)  ENV PHP_CFLAGS=-fstack-pr…   0B
3c59c17a324b       /bin/sh -c set -eux; apk add --no-cache --vi…   266MB
7a0e31d9f804        /bin/sh -c tar -Jxvf /usr/src/php.tar.xz -C …   114MB
d7cb999958ae       /bin/sh -c mkdir -p /usr/src/php; cd /usr/sr…   10.3MB
7d8970c4f4fe        /bin/sh -c #(nop)  ENV PHP_URL=https://www.p…   0B
9240b0b75cb6        /bin/sh -c set -eux; mkdir -p "$PHP_INI_DIR/…   0B
663500a7b308        /bin/sh -c #(nop)  ENV PHP_INI_DIR=/usr/loca…   0B
0396dfa3d284        /bin/sh -c set -eux; addgroup -g 82 -S www-d…   4.68kB
1a21cc439955       /bin/sh -c apk add --no-cache ca-certificate…   2.75MB
a24bb4013296        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           /bin/sh -c #(nop) ADD file:c92c248239f8c7b9b…   5.57MB

Получается так, что если скачать архив в одной команде RUN то это сохранится в один слой, если в слудующей команде компиляция, то это еще в один слой, если потом в следующем RUN подчищаем все, то по факту размер образа не уменьшается.

Какие могут быть решения для уменьшения размера образа?

  • Собирать с опцией --squash  в экспериментальном режиме, где все слои должно склеить, но у меня это отработало не совсем корректно и образ не особо уменьшило, видимо повторно этот же докерфайл собирает слишком поверхностно и надо сперва удалить раннее собранный, не стал копаться тут...
  • Можно весь скрипт в один RUN, там где скачал и распаковал файлы, в этой же команде RUN и чистить следы, удалять лишние пакеты требуемые для сборки...
  • Воспользоваться крутой утилитой Docker-slim

Docker-slim цменьшаем размер образа Docker

Попробуем уменьшить размер образа

 wget https://downloads.dockerslim.com/releases/1.32.0/dist_linux.tar.gz
 tar -zxvf dist_linux.tar.gz
 cd dist_linux/
 ./docker-slim

Набираем команду в утилите

build --target php-mp

В итоге в списке образов появится такой же, но с префиксом .slim

Было 477 мегабайт, стало 25, круто ведь, проверил, все работает корректно.

Теперь о том, как выгрузить этот образ, ведь он будет для наших кастомных задачек, публиковать на докер-хабе не собираюсь.

docker save php-mp.slim > myphp.tar

А на другом сервере

docker load --input myphp.tar

Получается мы можем забилдить контейнеры со своими конфигами, своими опциями сборки и модулями, скопировать на все сервера 25 мегабайтный архив, развернуть, запустить и все будет работать, никаких десятков пакетов в apt-get install, никаких монотонных правок php.ini и my.cnf, просто распаковать в докер, запустить и оно работает везде одинаково, везде с одним окружением... Магия!

Эта статья больше ознакомительная и шпаргалка для себя на будущее, скопировать готовый докер и запустить особо ума не надо, моя дальнейшая цель поработать с опциями, кое-что включить, а кое-что выключить, скачать сторонние библиотеки и собрать, больше не будет у меня такого что на новом сервере ставлю другую версию ПО на другой версии системы и потом спустя время ловлю что то тут то там забыл что-то настроить или установить, докер это круто.

Предварительная подготовка докерфайла

У любого человека при виде такого массивного Dockerfile возникнет вопрос - как же это вообще создать, чтобы не было ошибок, все просто, сперва запускаем консоль, предварительно сразу прокинув нужный порт, например 9000

 docker run -it --rm -p 9000:9000 -v /var/www/html:/var/www/html alpine:3.12 sh

Начинаем в нем выполнять команды типа apk add, apk del, make и все такое, в конце запускаем например php-fpm и тестируем из браузера, nginx ведь уже настроен, только остановите предварительно прошлый контейнер, который использует 9000 порт...

Немного про apline...

Устанавливать пакеты через apk add, удалять apk del

Если использовать параметр --no-cache, то кеш не будет создаваться и заполнять место, параметр --virtual это вообще интеерсная штука, после параметра virtual можно указать некое имя и потом по этому имени всё это снести что этой командой ставилось, например

apk add --no-cache --virtual kokoko nano glib zip ...
adk del kokoko

Это очень удобно, можно снести все что ставилось для компиляции и больше не требуется.

В ходе кастомной сборки у вас наверняка будут возникать моменты, когда какой-то библиотеки нет в системе, вы будете доустанавливать пакеты, поэтому во втором окне рядом откройте блокнот и актуализируйте команду установки, добавляя туда новые пакеты...

Когда все получилось, все действия в блокнотике отмечены, все команды и параметры, составляем наш докер-файл, и билдим, с первого раза может не получиться, например в одном RUN команды через ;, а все переносы экранируются \, когда многотекстовый документ надо заполнить, то так не получится:

echo 'f
fff
fff
gggg' > file.txt

Докер не понимает открытой строки, можно делать так как в примере выше было

{\
echo "f";\
echo "fff";\
echo "ggg";\
} | tee file.txt
обратите внимание на обратный слеш около переносов строк

Дальше все зависит от вашей фантазии, помните, эксперименты и опыты наше всё!

Показать комментарии