В коментарите под моята статия Имаше много искания да обясня, с какво е толкова ужасен описаният в нея Dockerfile.
Резюме на предишния епизод: двама разработчици в критичен срок съставят Dockerfile. По време на процеса при тях идва Ops Игор Иванович. Резултатният Dockerfile е толкова лош, че ИИ е на ръба на инфаркта.

Сега да видим какво не е наред с този Dockerfile.
И така, измина седмица.
Dev Петя се среща в столовата за чаша кафе с Ops Игор Иванович.
П: Игор Иванович, много ли сте зает? Искам да разбера къде сме сбъркали.
ИИ: Добре, не се срещат често разработчици, които се интересуват от експлоатацията.
Първо, нека да се договорим относно някои неща:
- Идеология на Docker: един контейнер — един процес.
- Колкото по-малък е контейнерът, толкова по-добре.
- Колкото повече се взима от кеша, толкова по-добре.
П: Но защо в един контейнер трябва да има един процес?
ИИ: Docker следи състоянието на процеса с pid 1, когато стартира контейнер. Ако процесът умре, Docker опитва да рестартира контейнера. Да предположим, че в контейнера са стартирани няколко приложения или основното приложение не се стартира с pid 1. Ако процесът умре, Docker няма да разбере за това.
Ако нямате повече въпроси, покажете вашия Dockerfile.
И Петя показа:
FROM ubuntu:latest
# Копираме изходния код
COPY ./ /app
WORKDIR /app
# Обновяваме списъка с пакети
RUN apt-get update
# Обновяваме пакетите
RUN apt-get upgrade
# Инсталираме необходимите пакети
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor
# Инсталираме bundler
RUN gem install bundler
# Инсталираме nodejs, използва се за изграждане на статиката
RUN curl -sL https://deb.nodesource.com/setup_9.x | sudo bash -
RUN apt-get install -y nodejs
# Инсталираме зависимостите
RUN bundle install --without development test --path vendor/bundle
# Почистваме кеша
RUN rm -rf /usr/local/bundle/cache/*.gem
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN rake assets:precompile
# Стартираме скрипта при старта на контейнера, който ще стартира всичко останало.
CMD ["/app/init.sh"]ИИ: Ох, нека да разгледаме нещата по ред. Да започнем с първия ред:
FROM ubuntu:latestВие вземате таг latest. Използването на тага latest води до непредсказуеми последствия. Представете си, че поддържачът на образа компилира нова версия на образа с различен списък от софтуер, този образ получава таг latest. И вашият контейнер в най-добрия случай спира да се компилира, а в най-лошия ловите бъгове, които преди това не сте имали.
Вие взимате образ с пълноценна ОС, която включва много ненужен софтуер, което увеличава размера на контейнера. И колкото повече софтуер, толкова повече уязвимости.
Освен това, колкото по-голям е образът, толкова повече място заема на хостинга и в registry (вие я настройвахте, нали?).
П: Разбира се, имаме registry.
ИИ: Та, за какво говорех...? Ах да, размерите... Нараства и натоварването на мрежата. За един образ това не е заметно, но когато става дума за непрекъсната сборка, тестове и деплой, разликата е осезаема. А ако нямате God’s mode на AWS, може да получите и космическа сметка.
Затова е важно да избирате най-подходящия образ, с конкретна версия и минимално количество софтуер. Например, вземете: FROM ruby:2.5.5-stretch
П: О, разбирам. Как мога да видя наличните образи? Как да разбера кой ми е необходим?
ИИ: Обикновено образите се вземат от , не го бъркайте с порнхаб :). За образа обикновено съществуват няколко сборки:
Alpine: образи, построени на минималистичната Linux система, само 5 MB. Недостатъкът е, че е изградена с собствена реализация на libc, стандартните пакети не работят. Времето за намиране и инсталиране на необходимия пакет ще бъде значително.
Scratch: основен образ, не се използва за изграждане на други образи. Той е предназначен изключително за стартиране на бинарни, подготвени данни. Идеален е за изпълнение на бинарни приложения, които включват всичко необходимо, например go-приложения.
На база на някоя ОС, напр. Ubuntu или Debian. Тук, предполагам, не е нужно да обяснявам.
ИИ: Сега трябва да инсталираме всички допълнителни пакети и да почистим кешовете. И веднага можем да изтрием apt-get upgrade. В противен случай, при всяка сборка, независимо от фиксирания таг на базовия имидж, ще получавате различни образи. Актуализирането на пакетите в образа е задача на мейнтейнера и крие промяна на тага.
П: Да, опитвах се да го направя, ето какво постигнах:
WORKDIR /app
COPY ./ /app
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
RUN rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*ИИ: Не зле, но и тук има какво да се подобри. Виж, тази команда:
RUN rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* … не изтрива данни от окончателния образ, а просто създава допълнителен слой без тези данни. Правилно е така:
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* Но това не е всичко. Какво имаш там, Ruby? Тогава не е необходимо в началото да копираш целия проект. Достатъчно е да копираш Gemfile и Gemfile.lock.
При такъв подход bundle install няма да се изпълнява за всяка промяна в изходниците, а само ако Gemfile или Gemfile.lock са променени.
Същите методи работят и за други езици с мениджъри на зависимости, като npm, pip, composer и други, базирани на файл със списък на зависимостите.
И накрая, помниш ли, в началото споменах идеологията на Docker "един контейнер — един процес"? Това означава, че supervisor не е необходим. Не трябва и да инсталираш systemd, по същите причини. Всъщност, Docker сам по себе си е supervisor. И когато опиташ да стартираш няколко процеса в него, е все едно в един процес на supervisor да стартираш няколко приложения.
При изграждането ще направиш единен образ, а след това ще стартираш необходимия брой контейнери, за да работи по един процес в всеки.
Но за това по-късно.
В: Изглежда, че разбрах. Виж какво се получава:
FROM ruby:2.5.5-stretch
WORKDIR /app
COPY Gemfile* /app
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY . /app
RUN rake assets:precompile
CMD ["bundle", "exec", "passenger", "start"]А как ще преопределим стартирането на демоните при стартиране на контейнера?
ИИ: Да, точно така. Между другото, можеш да използваш както CMD, така и ENTRYPOINT. А да разбереш в какво е разликата, това е твоето домашно. На тази тема в Хабра има добра статия. .
И така, да продължим. Сваляш файла за инсталиране на node, но по този начин няма никаква гаранция, че в него ще има това, от което имаш нужда. Трябва да добавиш валидация. Например, така:
RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x
&& echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c -
&& bash setup_9.x
&& rm -rf setup_9.x
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* С помощта на контролната сума можеш да провериш дали файлът е изтеглен правилно.
П: Но ако файлът се промени, сглобката няма да мине.
ИИ: Да, и, странно, това е плюс. Ще разбереш, че файлът се е променил и ще можеш да видиш какво е променено. Възможно е, например, да са добавили скрипт, който изтрива всичко, до което успее да се добере, или създава задна вратичка.
П: Благодаря. Значи, финалният Dockerfile ще изглежда така:
FROM ruby:2.5.5-stretch
WORKDIR /app
COPY Gemfile* /app
RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x
&& echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c -
&& bash setup_9.x
&& rm -rf setup_9.x
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY . /app
RUN rake assets:precompile
CMD ["bundle”, “exec”, “passenger”, “start"]П: Игор Ивaнович, благодаря за помощта. Вече трябва да бягам, трябва да направя още 10 коммита днес.
Игор Ивaнович, спирайки нетърпеливия си колега с поглед, взима глътка от силното кафе. Следвайки няколко секунди относно SLA 99.9% и код без грешки, задава въпрос.
ИИ: Къде съхранявате логовете?
П: Разбира се, в production.log. Между другото, как ще получим достъп до тях без ssh?
ИИ: Ако ги оставите в файлове, за вас вече е намерено решение. Командата docker exec позволява да изпълните всяка команда в контейнера. Например, можете да направите cat за логовете. А като използвате ключа -it и стартирате bash (ако е инсталиран в контейнера), ще получите интерактивен достъп до контейнера.
Но е по-добре да не се съхраняват логовете в файлове. Минимум, това води до неконтролируемо нарастване на контейнера, нали логовете не се ротират. Всички логове трябва да се изпращат в stdout. Там вече могат да бъдат разгледани с командата docker logs.
П: Игор Ивaнович, а какво ще кажете да изнесем логовете в монтирана директория, на физическа нода, подобно на потребителските данни?
ИИ: Добре, че не забравихте да изнесете данните, натоварени на диска на нодата. С логовете също е възможно, само не забравяйте да настроите ротацията.
Всичко, можеш да бягаш.
П: Игор Ивaнович, какво можете да препоръчате за четене?
ИИ: За начало прочетете , едва ли някой друг познава Docker по-добре от тях.
А ако искате да направите практика, запишете се на . Все пак теорията без практика е мъртва.
Източник: habr.com
