Изучив реальные Dockerfile и доступную литературу, я выделил несколько базовых bad practices. Для своего кейса я попытался осуществить как можно больше, не только 3, но из-за слишком простого проекта получилось не все.
- Выбор образа - стоит использовать официальные образы и alpine или с минимальными дополнительными пакетами, также использование тега: latest.
- Ипользовать
ADDвместоCOPY.ADD-примущемтвенно для распковки архивов или удаленного ресурса. Для всех остальных слуачев лучшеCOPY- недежнее без сюрпризов. - Не оптимизировать кэши и слои - пример: часто изменяющеся файлы копировать раньше(файлы проекта, раньше файла с зависомостями) и другие варианты.
- Не объединять
RUNинструкций - стоит обьеденятьRUNиструкции, так как каждая отдельная, отдельный слой занимающий больше места. - Не думать о безопасности в Dockerfile - не хранить секреты в dockerfile, не запускать приложени в dockerfile из под root.
- Не пользоваться healthcheck
- Не использовать multi-stage сборки - с компелируемыми языками програмирования полезно для ускорения сборки использовать multi-stage.
- Использовать shell-форму в Endpoint и
CMDвместоexec— при shell-форме могут возникнуть проблемы с сигналами, так как все процессы запускаются после PID 1 — интерпретатора в системе, в то время как при exec-форме PID 1 получает сам процесс, описанный в CMD или Endpoint.
- Выбор образа — использовал Python:latest вместо slim-версии. Разница: занимает на 1 гигабайт больше места, долго скачивается.
- Использовать
ADDвместоCOPY— в моем случае влияния нет, но так не стоит делать. - Не оптимизировать кэши и слои — файлы кода загружаются раньше списка зависимостей, хотя изменяются чаще и не может использоваться тот же кэш.
- Запуск из-под root — небезопасно, 4 моих года безопасника испытывали физическую боль, выполняя этот пункт. Решение: создание пользователя без root и запуск с его правами проекта.
- Не использовал healthcheck — хотя бы так организовать мониторинг доступности.
- Использовать shell-форму в Endpoint и
CMDвместоexec— в моем случае не влияло, но не стоит так делать.
- Не устанавливать лимиты по контейнеру: запуск контейнера без лимитов может привести к тому, что он использует все возможные ресурсы и не оставит другим. Чтобы этого избежать, стоит использовать
--memory, --cpus, --pids-limit. - Использовать проброс docker.sock, таким образом, даже если приложение внутри контейнера запущено не от root, у потенциального злоумышленника появляется возможность получить доступ к инфраструктуре, используя docker API.
Для продолжения выполнения задания пришлось усложнить проект, для этого sqllite был заменен на postgres. Были выбраны следующие bad practices для реализации:
- Хранение секретов в файле сборки.
- Запуск базы данных с избыточными привилегиями.
- Использование базовой сети.
- Не использовать volume при работе с БД.
То, как исправил и что добавил в good docker compose:
- Использую secrets и немного изменил app.py.
- Убрал запуск с избыточными привилегиями.
- Создал отдельную сеть, куда и включил оба сервиса.
- Использовал volume для БД.
Сетевая изоляция: Изначально я помещал сервисы в одну сеть, поэтому сервисы могли обращаться друг к другу по имени. Для изоляции двух сервисов мне пришлось создать вторую сеть и разделить сервисы по разным сетям, таким образом у них нет общей сети и docker DNS не может резолвить запросы по имени сервиса.
Этот вариант изоляции работает, так как Docker специально изолирует все виртуальные сети и docker DNS работает только в рамках одной сети. Таким образом обеспечивается безопасность и сеть получается плоской, что упрощает работу с сетями внутри контейнеров.