
Часто бывает такое, что нужен к примеру самый новый 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
собираем образ 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
Дальше все зависит от вашей фантазии, помните, эксперименты и опыты наше всё!