
Docker за последнее время стал очень популярен за счет своей производительности, отказоустойчивости и, главное, простоты.
Сегодня можно найти тысячи образов в hub.docker.com. За счет своей простоты в создании образов, буквально за пол часа можно начать вносить свой вклад.
Но многие забывают о best practices, и за счет этого docker hub наполнился огромным количеством не самых лучших образов.
В этой статье я хочу описать на сколько просто и полезно создавать образы используя Best Practices на примере.
В качестве примера я выбрал нетривиальный образ с oracle 11g xe GitHub docker-hub.
В исходном проекте можно определить слабые места и недоработки, отсортированные по основным пунктам с best practices:
Использование .dockerignore
Очень полезный функционал, но, к сожалению, многие о нем не знают и не пользуются.
В итоге
oracle-xe_11.2.0-1.0_amd64.debaa
oracle-xe_11.2.0-1.0_amd64.debab
oracle-xe_11.2.0-1.0_amd64.debac
.git
.gitignore
Запускать только один процесс на контейнер
Это довольно распространенная ошибка, и допускается за счет того, что люди не до конца понимают принципы работы и риски.
В первую очередь в глаза кидается SSHD и не очень правильная инструкция CMD
CMD sed -i -E "s/HOST = [^)]+/HOST = $HOSTNAME/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora; \
service oracle-xe start; \
/usr/sbin/sshd -D
Минусы использования подобного подхода можно обсуждать очень долго, особенно если пользователь захочет «кастомизировать» входящую команду.
В первую очередь удаляем SSHD так как он нам не нужен, даже если нам будет необходимо выполнить debug или просто подключится к консоле контейнера лучше использовать docker exec -it ${CONTAINER_ID} /bin/bash
Также очевидно, что при остановке контейнера Gracefully останавливается только SSHD, в то время, как сама база останавливается по TERM сигналу как процесс без паррента, что не есть хорошо, особенно для базы данных, особенно для Oracle DB.
по «sed» и «service start» уже можно предположить, что просто не будет, и разумно будет перенести ввесь описанный функционал в entrypoint.sh
При подготовке ENTRYPOINT был вынужден использовать несколько
Минимизация количества слоев
Этот пункт очень прост, но в то же время очень важен, так как Docker работает по наслоению инкрементальных изменений в ФС по одной на каждую инструкцию, вот пример рационального использования, главное стараться оставлять код читабельным и вместить все изменения в одну RUN инструкцию
# Prepare to install Oracle
RUN apt-get update && apt-get install -y -q libaio1 net-tools bc curl && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* &&\
ln -s /usr/bin/awk /bin/awk &&\
mkdir /var/lock/subsys &&\
chmod 755 /sbin/chkconfig &&\
/oracle-install.sh
Функционал по установке oracle перенесен в sh скрипт в пользу читабельности.
Избегать установки лишних не самых необходимых пакетов
Помимо отказа от установки лишних пакетов так же крайне важно очищать после себя установочные файлы, кеши и прочее в одной инструкции, чтобы исключить ненужные наслоения инкрементальных слоев, иначе образ будет вдвойне тяжелей.
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* /download/directory
Контейнер должен быть эфемерный
Это один из самых сложных и важных моментов. Под понятием «Эфемерный» подразумевается, что при старте контейнера, а затем его остановки с удалением, следующий запуск должен быть способным продолжать работу предыдущего с минимальной конфигурацией.
В нашем случае это файлы базы данных (принцип работы как холодный бэкап) с возможностью его использования при старте нового контейнера.
Данный подход и Docker позволяет нам легко и быстро создавать резервную копию, моментально откатится и клонировать всю базу.
Также не мало важно вынести базовые параметры как конфигурирование через ENV переменные.
В итоге у меня получился вот такой ENTRYPOINT
#!/bin/bash
# Prevent owner issues on mounted folders
chown -R oracle:dba /u01/app/oracle
rm -f /u01/app/oracle/product
ln -s /u01/app/oracle-product /u01/app/oracle/product
# Update hostname
sed -i -E "s/HOST = [^)]+/HOST = $HOSTNAME/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora
sed -i -E "s/PORT = [^)]+/PORT = 1521/g" /u01/app/oracle/product/11.2.0/xe/network/admin/listener.ora
echo "export ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe" > /etc/profile.d/oracle-xe.sh
echo "export PATH=\$ORACLE_HOME/bin:\$PATH" >> /etc/profile.d/oracle-xe.sh
echo "export ORACLE_SID=XE" >> /etc/profile.d/oracle-xe.sh
. /etc/profile
case "$1" in
'')
#Check for mounted database files
if [ "$(ls -A /u01/app/oracle/oradata)" ]; then
echo "found files in /u01/app/oracle/oradata Using them instead of initial database"
echo "XE:$ORACLE_HOME:N" >> /etc/oratab
chown oracle:dba /etc/oratab
chown 664 /etc/oratab
printf "ORACLE_DBENABLED=false\nLISTENER_PORT=1521\nHTTP_PORT=8080\nCONFIGURE_RUN=true\n" > /etc/default/oracle-xe
rm -rf /u01/app/oracle-product/11.2.0/xe/dbs
ln -s /u01/app/oracle/dbs /u01/app/oracle-product/11.2.0/xe/dbs
else
echo "Database not initialized. Initializing database."
printf "Setting up:\nprocesses=$processes\nsessions=$sessions\ntransactions=$transactions\n"
echo "If you want to use different parameters set processes, sessions, transactions env variables and consider this formula:"
printf "processes=x\nsessions=x*1.1+5\ntransactions=sessions*1.1\n"
mv /u01/app/oracle-product/11.2.0/xe/dbs /u01/app/oracle/dbs
ln -s /u01/app/oracle/dbs /u01/app/oracle-product/11.2.0/xe/dbs
#Setting up processes, sessions, transactions.
sed -i -E "s/processes=[^)]+/processes=$processes/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
sed -i -E "s/processes=[^)]+/processes=$processes/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
sed -i -E "s/sessions=[^)]+/sessions=$sessions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
sed -i -E "s/sessions=[^)]+/sessions=$sessions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
sed -i -E "s/transactions=[^)]+/transactions=$transactions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
sed -i -E "s/transactions=[^)]+/transactions=$transactions/g" /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
printf 8080\\n1521\\noracle\\noracle\\ny\\n | /etc/init.d/oracle-xe configure
echo "Database initialized. Please visit http://#containeer:8080/apex to proceed with configuration"
fi
/etc/init.d/oracle-xe start
echo "Database ready to use. Enjoy! ;)"
##
## Workaround for graceful shutdown. oracle... ‿( ́ ̵ _-`)‿
##
while [ "$END" == '' ]; do
sleep 1
trap "/etc/init.d/oracle-xe stop && END=1" INT TERM
done
;;
*)
echo "Database is not configured. Please run /etc/init.d/oracle-xe configure if needed."
$1
;;
esac
Резюме
В итоге, следуя Best Practices, мы получили целый ряд преимуществ:
- Размер образа уменьшился на 3GB (с 3.8Gb до 825Mb)
- Поддержка монтирования и повторного использования дата-файлов
- Graceful остановка сервиса
- Возможности для более тонкой настройке базы через параметры при старте контейнера
Результаты работы и детали решения проблем вы можете найти на github и hub.docker.com
Спасибо за внимание.