Контейнер - не віртуальна машина: він ділить ядро з хостом, і помилки конфігурації можуть дати вихід за його межі. Основні правила:
1. Не працювати від root.
FROM php:8.5-fpm-alpine
# ...
RUN addgroup -S app && adduser -S app -G app
USER app
Вразливість у застосунку, що дає виконання коду, в контейнері від root - це root у контейнері, і значно ближче до хоста.
2. Мінімальний образ. Alpine чи slim-варіанти, без компіляторів, curl, git і пакетів «на всяк випадок». Менше програм - менше вразливостей і інструментів для зловмисника. Багатоетапне збирання (multi-stage) - залежності й ассети збираються в одному етапі, а в фінальний образ потрапляє лише результат.
3. Жодних секретів в образі. COPY .env . чи ARG API_KEY лишаються в шарах образу назавжди - будь-хто з доступом до образу їх дістане (docker history, розпакування шарів). Секрети - змінними оточення чи змонтованими файлами під час запуску; для збирання - --mount=type=secret. Плюс .dockerignore з .env, .git, node_modules.
4. Файлова система лише для читання, де можливо (read_only: true у Compose), з окремими томами для storage/ і тимчасових файлів.
5. Обмеження прав контейнера:
cap_drop: [ALL]і лише потрібні можливості;no-new-privileges: true;- ніколи
privileged: trueі монтування/var/run/docker.sockу контейнер застосунку - це фактично root на хості.
6. Оновлення й сканування:
- закріплені версії базових образів (а ще краще - дайджест) і регулярне перезбирання з оновленою базою;
- сканування вразливостей образів у CI: Trivy, Grype, Docker Scout.
7. Мережа й ресурси: контейнер бази не публікує порт назовні, ліміти пам'яті й CPU (захист від виснаження ресурсів).
8. Health check і журнали в stdout - щоб оркестратор бачив стан, а журнали не накопичувалися в контейнері.
Пастка закріплення: «плаваючий» тег (php:8.5-fpm) може раптово принести несумісні зміни, а закріплений назавжди - перестати отримувати виправлення безпеки. Рішення - закріплена версія + автоматичні PR оновлень (Renovate, Dependabot).