Docker в 2026 году стал стандартом для развёртывания веб-проектов — я лично перевёл уже больше 50 сайтов в контейнеры за последний год. И честно говоря, теперь не представляю, как раньше обходился без этой технологии.
Зачем нужна контейнеризация сайтов в 2026
Когда я начинал работать с Docker в 2019, это была скорее игрушка для DevOps-инженеров. Сейчас — это необходимость. У меня был клиент с интернет-магазином на Laravel, который работал на старом VPS с PHP 7.4 и MySQL 5.7. Каждое обновление превращалось в кошмар: то зависимости не совпадают, то версии не подходят.
После перевода на Docker всё изменилось кардинально. Теперь разработка ведётся в точно такой же среде, как и продакшн. Новые разработчики поднимают проект за 5 минут командой docker-compose up. А деплой стал предсказуемым на 100%.
Основные преимущества контейнеризации:
- Изолированность — каждый сайт живёт в своём окружении
- Масштабируемость — добавляем новые контейнеры по мере роста нагрузки
- Портируемость — один раз настроил, запускается везде одинаково
- Быстрое восстановление — если что-то сломалось, перезапускаем контейнер за секунды
- Версионирование инфраструктуры — всё описано в коде
На деле Docker решает проблему "у меня работает, а у вас нет". Теперь либо работает у всех, либо не работает вообще нигде. Это гораздо проще диагностировать и чинить.
Архитектура Docker-контейнеров для веб-сайтов
Я обычно использую многоконтейнерную архитектуру. Один контейнер — одна задача. Это классический подход Docker, который проверен временем.
Стандартная схема для веб-проекта выглядит так:
- web — Nginx или Apache для отдачи статики и проксирования
- app — PHP-FPM с кодом приложения
- db — MySQL или PostgreSQL
- redis — для кеширования и сессий
- queue — для фоновых задач (Laravel Queue, Bitrix Agents)
У одного клиента была нагруженная CRM-система на Битрикс с интеграциями. Мы разбили её на 7 контейнеров: веб-сервер, PHP для публичной части, отдельный PHP для админки (с увеличенными лимитами памяти), MySQL, Redis, очереди и контейнер для cron-задач. Система стала работать стабильнее и быстрее на 40%.
Важный момент — не стоит запихивать всё в один контейнер. Да, можно установить в Ubuntu-образ и Nginx, и PHP, и MySQL, но это противоречит философии Docker. Каждый контейнер должен быть максимально простым и выполнять одну функцию.
Для хранения данных использую именованные тома (named volumes). Они переживают пересоздание контейнеров и легко бэкапятся. Конфиги и код монтирую через bind mounts в режиме разработки, а в продакшне копирую внутрь образов.
Настройка docker-compose для веб-проекта
Docker Compose — это то, что делает работу с многоконтейнерными приложениями простой. Я всегда начинаю настройку с файла docker-compose.yml в корне проекта.
Вот пример конфигурации для сайта на Laravel с MySQL и Redis:
version: '3.8'
services:
web:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/sites:/etc/nginx/conf.d:ro
- ./public:/var/www/html/public:ro
- ./storage/app/public:/var/www/html/storage/app/public:ro
- ssl_certs:/etc/nginx/ssl
depends_on:
- app
networks:
- app-network
app:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- ./:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/99-custom.ini
environment:
- DB_HOST=db
- REDIS_HOST=redis
- APP_ENV=production
depends_on:
- db
- redis
networks:
- app-network
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/custom.cnf
ports:
- "3306:3306"
networks:
- app-network
redis:
image: redis:7.2-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- app-network
queue:
build:
context: .
dockerfile: docker/php/Dockerfile
command: php artisan queue:work --tries=3 --timeout=60
volumes:
- ./:/var/www/html
environment:
- DB_HOST=db
- REDIS_HOST=redis
depends_on:
- db
- redis
networks:
- app-network
restart: unless-stopped
scheduler:
build:
context: .
dockerfile: docker/php/Dockerfile
command: /bin/sh -c "while true; do php artisan schedule:run; sleep 60; done"
volumes:
- ./:/var/www/html
environment:
- DB_HOST=db
- REDIS_HOST=redis
depends_on:
- db
- redis
networks:
- app-network
restart: unless-stopped
volumes:
mysql_data:
redis_data:
ssl_certs:
networks:
app-network:
driver: bridge
Ключевые моменты в этой конфигурации:
Переменные окружения — все секреты выношу в .env файл. Никогда не прописываю пароли прямо в docker-compose.yml. Это базовая безопасность.
Именованные тома — mysql_data и redis_data переживут пересоздание контейнеров. Данные не потеряются.
Сеть — создаю отдельную сеть app-network. Контейнеры общаются между собой по именам сервисов, а не IP-адресам.
Restart policies — для критически важных сервисов типа очередей ставлю unless-stopped. Если контейнер упал, Docker автоматически его перезапустит.
Dockerfile для PHP-приложений
Создание правильного Dockerfile — это искусство. Я потратил месяцы на то, чтобы найти оптимальный баланс между размером образа, скоростью сборки и функциональностью.
Вот мой проверенный Dockerfile для PHP 8.3 с всеми нужными расширениями:
FROM php:8.3-fpm-alpine
# Устанавливаем системные зависимости
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libxml2-dev \
libzip-dev \
freetype-dev \
libjpeg-turbo-dev \
icu-dev \
oniguruma-dev \
postgresql-dev \
nginx \
supervisor
# Устанавливаем PHP расширения
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
bcmath \
exif \
gd \
intl \
mbstring \
opcache \
pdo \
pdo_mysql \
pdo_pgsql \
xml \
zip
# Устанавливаем Redis расширение
RUN pecl install redis-6.0.2 \
&& docker-php-ext-enable redis
# Устанавливаем Composer
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
# Создаём пользователя для запуска приложения
RUN addgroup -g 1000 -S www-data \
&& adduser -u 1000 -D -S -G www-data www-data
# Копируем конфигурации
COPY docker/php/php.ini /usr/local/etc/php/conf.d/99-custom.ini
COPY docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf
# Устанавливаем рабочую директорию
WORKDIR /var/www/html
# Копируем composer файлы и устанавливаем зависимости
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
# Копируем код приложения
COPY . .
# Устанавливаем права доступа
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Оптимизируем Laravel
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
Этот Dockerfile оптимизирован для продакшна. Я использую Alpine Linux как базовый образ — он весит всего 5MB против 100+ MB у Ubuntu. Все зависимости устанавливаю одной командой RUN, чтобы минимизировать количество слоёв.
Важный момент — порядок команд. Сначала копирую composer.json и устанавливаю зависимости, только потом копирую код. Это позволяет Docker переиспользовать кеш слоёв, если код изменился, а зависимости остались те же.
Для разработки создаю отдельный Dockerfile.dev:
FROM php:8.3-fpm-alpine
# Те же системные зависимости + Xdebug
RUN apk add --no-cache git curl libpng-dev libxml2-dev libzip-dev \
&& docker-php-ext-install pdo pdo_mysql zip gd
# Устанавливаем Xdebug для отладки
RUN pecl install xdebug-3.3.1 \
&& docker-php-ext-enable xdebug
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
WORKDIR /var/www/html
# В dev-режиме не копируем код, а монтируем через volume
У меня есть клиент, который разрабатывает сложную ERP-систему. Сборка продакшн-образа у них занимала 15 минут. После оптимизации Dockerfile время сократилось до 3 минут. Секрет в правильном кешировании и multi-stage сборке.
Настройка Nginx для Docker-контейнеров
Nginx в Docker — это отдельная тема. Я предпочитаю выносить его в отдельный контейнер, который проксирует запросы к PHP-FPM. Это даёт гибкость в настройке и позволяет легко масштабировать backend.
Конфигурация nginx.conf для Docker:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Логирование
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
# Оптимизации
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;
# Gzip сжатие
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Upstream для PHP-FPM
upstream php-fpm {
server app:9000;
}
# Включаем конфигурации сайтов
include /etc/nginx/conf.d/*.conf;
}
А вот конфигурация конкретного сайта (sites/default.conf):
server {
listen 80;
server_name example.com www.example.com;
# Редирект на HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
root /var/www/html/public;
index index.php index.html;
# SSL конфигурация
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Кеширование статики
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Основная обработка PHP
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-fpm;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Оптимизации FastCGI
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
}
# Запрет доступа к системным файлам
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~* (composer\.json|composer\.lock|\.env)$ {
deny all;
access_log off;
log_not_found off;
}
}
Ключевая особенность — upstream php-fpm указывает на имя сервиса app из docker-compose.yml. Docker автоматически резолвит это имя в IP-адрес контейнера.
Я всегда настраиваю логирование. В контейнерах логи пишутся в stdout/stderr, и Docker их автоматически собирает. Это удобно для централизованного мониторинга через ELK stack или Grafana.
Управление базами данных в контейнерах
База данных в Docker — это всегда вопрос сохранности данных. Я видел проекты, где разработчики случайно удаляли контейнер с базой и теряли всё. Поэтому персистентность данных — это критически важно.
Для MySQL 8.0 моя стандартная конфигурация выглядит так:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/custom.cnf:ro
- ./docker/mysql/init:/docker-entrypoint-initdb.d:ro
command: --default-authentication-plugin=mysql_native_password
ports:
- "3306:3306"
networks:
- app-network
restart: unless-stopped
В файле my.cnf настраиваю оптимизации под конкретный проект:
[mysqld]
# Основные настройки
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Оптимизации для Docker
bind-address = 0.0.0.0
skip-host-cache
skip-name-resolve
# Увеличиваем лимиты для больших запросов
max_allowed_packet = 64M
max_connections = 200
wait_timeout = 600
# Логирование медленных запросов
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2
# Настройки для репликации (если нужна)
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
Для инициализации базы создаю SQL-скрипты в папке docker/mysql/init. MySQL автоматически выполнит их при первом запуске:
-- 01-create-databases.sql
CREATE DATABASE IF NOT EXISTS `app_production` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS `app_testing` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 02-create-users.sql
CREATE USER 'app_user'@'%' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON `app_production`.* TO 'app_user'@'%';
GRANT ALL PRIVILEGES ON `app_testing`.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
У одного клиента была проблема с производительностью MySQL в Docker. База данных работала в 3 раза медленнее, чем на хосте. Оказалось, что виноват был файловый драйвер. После перехода на volume driver с прямым доступом к диску производительность стала даже лучше, чем на хосте.
Для PostgreSQL настройка похожая, но есть нюансы:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
command: postgres -c config_file=/etc/postgresql/postgresql.conf
ports:
- "5432:5432"
Важно не забывать про бэкапы. Я настраиваю автоматические бэкапы через отдельный контейнер, который запускается по расписанию. Подробнее об этом писал в статье про автоматическое резервное копирование.
Оптимизация производительности контейнеров
Docker из коробки работает не всегда оптимально. За годы практики я выработал набор правил, которые позволяют выжать максимум производительности из контейнеризованных приложений.
Первое правило — правильные лимиты ресурсов. Не ограничивайте контейнеры слишком жёстко, но и не давайте им съедать всю память сервера:
app:
build: .
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 512M
cpus: '0.5'
Второе — оптимизация образов. Я использую multi-stage сборку для минимизации размера финальных образов:
# Стадия сборки
FROM node:18-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Стадия PHP
FROM php:8.3-fpm-alpine AS php-stage
RUN docker-php-ext-install pdo pdo_mysql
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
# Финальная стадия
FROM php:8.3-fpm-alpine
COPY --from=php-stage /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=build-stage /app/node_modules ./node_modules
COPY . /var/www/html
Третье — настройка PHP-FPM. Стандартные настройки не подходят для продакшна:
[www]
user = www-data
group = www-data
listen = 9000
listen.owner = www-data
listen.group = www-data
; Процессы
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
; Оптимизации
pm.process_idle_timeout = 60s
request_terminate_timeout = 300s
request_slowlog_timeout = 30s
slowlog = /var/log/php-fpm-slow.log
; Мониторинг
pm.status_path = /fpm-status
ping.path = /fpm-ping
Четвёртое — использование правильного storage driver. На продакшн-серверах я всегда использую overlay2 с direct-lvm:
{
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Был случай на высоконагруженном проекте — сайт тормозил под нагрузкой. После профилирования выяснилось, что узким местом был именно storage driver. Переход с aufs на overlay2 дал прирост производительности на 35%.
Пятое — кеширование. Redis в отдельном контейнере обязательно, но также настраиваю OPcache для PHP:
; OPcache настройки
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=0
opcache.fast_shutdown=1
Также использую Redis для кеширования на уровне приложения. Подробнее про настройку кеширования писал в статье про Redis и Memcached.
CI/CD интеграция с Docker
Docker революционизировал процесс деплоя. Теперь я могу гарантировать, что код, который работает в разработке, будет точно так же работать в продакшне. Настройка CI/CD с Docker стала стандартной практикой.
Вот пример pipeline для GitLab CI:
stages:
- build
- test
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_BUILDKIT: 1
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
test:
stage: test
script:
- docker-compose -f docker-compose.test.yml up -d
- docker-compose -f docker-compose.test.yml exec -T app php artisan test
- docker-compose -f docker-compose.test.yml exec -T app php vendor/bin/phpstan analyse
after_script:
- docker-compose -f docker-compose.test.yml down -v
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
deploy_staging:
stage: deploy
script:
- ssh $STAGING_USER@$STAGING_HOST "cd /var/www/staging && git pull origin develop"
- ssh $STAGING_USER@$STAGING_HOST "cd /var/www/staging && docker-compose pull"
- ssh $STAGING_USER@$STAGING_HOST "cd /var/www/staging && docker-compose up -d --remove-orphans"
- ssh $STAGING_USER@$STAGING_HOST "cd /var/www/staging && docker-compose exec -T app php artisan migrate --force"
only:
- develop
environment:
name: staging
url: https://staging.example.com
deploy_production:
stage: deploy
script:
- ssh $PROD_USER@$PROD_HOST "cd /var/www/production && git pull origin main"
- ssh $PROD_USER@$PROD_HOST "cd /var/www/production && docker-compose pull"
- ssh $PROD_USER@$PROD_HOST "cd /var/www/production && docker-compose up -d --remove-orphans"
- ssh $PROD_USER@$PROD_HOST "cd /var/www/production && docker-compose exec -T app php artisan migrate --force"
- ssh $PROD_USER@$PROD_HOST "cd /var/www/production && docker-compose exec -T app php artisan config:cache"
only:
- main
environment:
name: production
url: https://example.com
when: manual
Этот pipeline автоматически:
- Собирает Docker-образ из исходного кода
- Запускает тесты в изолированной среде
- Деплоит на staging при пуше в develop
- Деплоит на продакшн при ручном запуске из main
Для GitHub Actions конфигурация похожая:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/production
docker-compose pull
docker-compose up -d --remove-orphans
docker-compose exec -T app php artisan migrate --force
docker system prune -f
Ключевая фишка — использование кеша сборки. Это позволяет собирать образы в разы быстрее, переиспользуя неизменённые слои.
У одного клиента деплой занимал 20 минут. После внедрения Docker и настройки правильного CI/CD время сократилось до 3 минут. А главное — деплой стал предсказуемым и безопасным. Если что-то идёт не так, можно откатиться к предыдущей версии за 30 секунд.
Безопасность Docker-контейнеров
Безопасность контейнеров — это не только правильная настройка Docker, но и культура разработки. Я видел проекты, где в образы зашивали пароли от баз данных или API-ключи. Это недопустимо.
Основные принципы безопасности:
1. Никогда не запускайте контейнеры от root. Создавайте отдельного пользователя:
RUN addgroup -g 1000 -S appuser \
&& adduser -u 1000 -D -S -G appuser appuser
USER appuser
2. Используйте секреты для чувствительных данных:
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
external: true
api_key:
external: true
3. Ограничивайте capabilities контейнеров:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
security_opt:
- no-new-privileges:true
4. Используйте read-only файловую систему где возможно:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp
- /var/run
- /var/cache/nginx
5. Сканируйте образы на уязвимости. Я использую Trivy для этого:
trivy image myapp:latest
В CI/CD добавляю этап сканирования:
security_scan:
stage: test
script:
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Был случай — клиент использовал устаревший базовый образ Ubuntu 18.04 с кучей уязвимостей. После сканирования Trivy нашёл 47 критических уязвимостей. Переход на Alpine Linux решил проблему — уязвимостей стало 0.
6. Настройте правильное логирование и мониторинг:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "service,version"
7. Используйте Docker Bench для Security:
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
--label docker_bench_security \
docker/docker-bench-security
Этот инструмент проверит вашу Docker-установку на соответствие стандартам безопасности CIS.
Мониторинг и отладка контейнеров
Мониторинг Docker-контейнеров кардинально отличается от мониторинга обычных серверов. Контейнеры могут создаваться и удаляться динамически, поэтому нужны специализированные инструменты.
Я использую связку Prometheus + Grafana + cAdvisor для мониторинга:
monitoring:
prometheus:
image: prom/prometheus:v2.47.0
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.0
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
grafana:
image: grafana/grafana:10.1.0
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
Конфигурация Prometheus (prometheus.yml):
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'docker-containers'
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: [__meta_docker_container_name]
regex: '/(.*)'
target_label: container_name
Для централизованного сбора логов использую ELK stack:
logging:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.9.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
- xpack.security.enabled=false
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
logstash:
image: docker.elastic.co/logstash/logstash:8.9.0
volumes:
- ./logging/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro
depends_on:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:8.9.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
Для отладки контейнеров в процессе разработки использую несколько команд:
# Просмотр логов контейнера
docker logs -f container_name
# Подключение к контейнеру
docker exec -it container_name /bin/sh
# Мониторинг ресурсов в реальном времени
docker stats
# Информация о контейнере
docker inspect container_name
# Просмотр процессов внутри контейнера
docker exec container_name ps aux
У одного клиента была проблема с утечкой памяти в PHP-приложении. Стандартные инструменты не помогали — память росла постепенно и незаметно. Настроил детальный мониторинг через Prometheus, и за неделю удалось выявить паттерн: утечка происходила только при обработке больших файлов. Проблема была в неправильной работе с временными файлами.
Также настраиваю алерты в Grafana:
{
"alert": {
"name": "High Memory Usage",
"message": "Container memory usage is above 90%",
"frequency": "10s",
"conditions": [
{
"query": {
"queryType": "",
"refId": "A",
"expr": "(container_memory_usage_bytes / container_spec_memory_limit_bytes) * 100 > 90"
}
}
],
"executionErrorState": "alerting",
"noDataState": "no_data",
"for": "5m"
}
}
Мониторинг здоровья приложения настраиваю через health checks:
app:
image: myapp:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
Это позволяет Docker автоматически перезапускать нездоровые контейнеры.
Масштабирование и оркестрация
Когда нагрузка растёт, одного сервера становится мало. Docker Swarm и Kubernetes позволяют масштабировать приложения горизонтально. Я предпочитаю начинать с Docker Swarm — он проще в настройке и вполне подходит для большинства проектов.
Инициализация Docker Swarm кластера:
# На главном узле
docker swarm init --advertise-addr 192.168.1.10
# На worker-узлах
docker swarm join --token TOKEN 192.168.1.10:2377
Docker Compose файл для Swarm режима:
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- frontend
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
placement:
constraints:
- node.role == worker
app:
image: myapp:latest
networks:
- frontend
- backend
environment:
- DB_HOST=db
- REDIS_HOST=redis
deploy:
replicas: 5
resources:
limits:
memory: 512M
reservations:
memory: 256M
update_config:
parallelism: 2
delay: 10s
failure_action: rollback
restart_policy:
condition: on-failure
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
volumes:
- mysql_data:/var/lib/mysql
networks:
- backend
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
networks:
frontend:
driver: overlay
backend:
driver: overlay
volumes:
mysql_data:
Деплой в Swarm:
docker stack deploy -c docker-compose.yml myapp
Swarm автоматически распределит контейнеры по узлам кластера и обеспечит load balancing между репликами.
Для мониторинга Swarm кластера добавляю Portainer:
portainer:
image: portainer/portainer-ce:latest
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
deploy:
placement:
constraints:
- node.role == manager
У одного клиента был высоконагруженный API на Laravel. Одного сервера не хватало — в пиковые часы response time доходило до 5 секунд. Развернул кластер из 3 серверов в Docker Swarm, настроил автоскейлинг. Теперь система автоматически масштабируется от 3 до 15 реплик в зависимости от нагрузки. Response time стабильно держится в районе 200ms.
Для автоскейлинга использую внешние инструменты типа Docker Flow Proxy или Traefik:
traefik:
image: traefik:v3.0
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
deploy:
placement:
constraints:
- node.role == manager
app:
image: myapp:latest
deploy:
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`example.com`)
- traefik.http.services.app.loadbalancer.server.port=80
Traefik автоматически обнаруживает новые контейнеры и добавляет их в load balancer.
Если проект совсем большой, то переходим на Kubernetes. Но это уже отдельная большая тема, которая требует специализированных знаний. Для 95% проектов Docker Swarm более чем достаточно.
Важный момент — не забывайте про персистентные данные при масштабировании. База данных должна быть одна, а статические файлы лучше выносить в объектное хранилище типа S3 или настроить CDN.
Docker изменил подход к разработке и деплою веб-приложений. То, что раньше требовало недели настройки, теперь делается за часы. Контейнеризация стала стандартом индустрии, и игнорировать эту технологию в 2026 году просто нельзя. Начинайте с простых проектов, изучайте best practices, и очень скоро вы не сможете представить разработку без Docker.
Нужна помощь с настройкой Docker для вашего проекта?
Наши эксперты помогут настроить эффективную контейнеризацию для вашего сайта с учетом современных требований.