Настройка Docker-контейнеризации для сайта в 2026 году

Docker в 2026 году стал стандартом для развёртывания веб-проектов — я лично перевёл уже больше 50 сайтов в контейнеры за последний год. И честно говоря, теперь не представляю, как раньше обходился без этой технологии.

Зачем нужна контейнеризация сайтов в 2026

Когда я начинал работать с Docker в 2019, это была скорее игрушка для DevOps-инженеров. Сейчас — это необходимость. У меня был клиент с интернет-магазином на Laravel, который работал на старом VPS с PHP 7.4 и MySQL 5.7. Каждое обновление превращалось в кошмар: то зависимости не совпадают, то версии не подходят.

После перевода на Docker всё изменилось кардинально. Теперь разработка ведётся в точно такой же среде, как и продакшн. Новые разработчики поднимают проект за 5 минут командой docker-compose up. А деплой стал предсказуемым на 100%.

Основные преимущества контейнеризации:

На деле Docker решает проблему "у меня работает, а у вас нет". Теперь либо работает у всех, либо не работает вообще нигде. Это гораздо проще диагностировать и чинить.

💡
Из практики: За 2025 год я ни разу не получил тикет "сайт работает криво после обновления" от клиентов, которые перешли на Docker. До этого такие обращения приходили каждую неделю.

Архитектура Docker-контейнеров для веб-сайтов

Я обычно использую многоконтейнерную архитектуру. Один контейнер — одна задача. Это классический подход Docker, который проверен временем.

Стандартная схема для веб-проекта выглядит так:

У одного клиента была нагруженная 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 автоматически его перезапустит.

⚠️
Внимание: Не монтируйте код приложения в продакшне через volumes. Это создаёт лишнюю зависимость от файловой системы хоста. Лучше копировать код внутрь образа на этапе сборки.

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.

ℹ️
Совет: Используйте nginx:alpine образ вместо обычного nginx. Он весит в 10 раз меньше и содержит только необходимые компоненты. Для веб-сервера этого более чем достаточно.

Управление базами данных в контейнерах

База данных в 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.

💡
Мониторинг производительности: Обязательно настройте мониторинг контейнеров через cAdvisor + Prometheus + Grafana. Это позволит отслеживать использование ресурсов и вовремя выявлять проблемы.

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 автоматически:

Для 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.

⚠️
Важно: Регулярно обновляйте базовые образы. Настройте автоматические уведомления о новых версиях через Dependabot или аналогичные сервисы. Устаревший образ — это потенциальная дыра в безопасности.

Мониторинг и отладка контейнеров

Мониторинг 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 для вашего проекта?

Наши эксперты помогут настроить эффективную контейнеризацию для вашего сайта с учетом современных требований.

П
Павел
Веб-разработчик · 10+ лет опыта · Bitrix, WordPress, Laravel

Читайте также

Как настроить CDN для сайта: пошаговое руководство Технический долг сайта: что это и как бороться Настройка Google Analytics и Яндекс.Метрики на сайте