Docker-compose в GitHub Actions: настройка CI/CD для контейнеров

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Для разработчиков, работающих с Docker и GitHub Actions
  • Для специалистов в области DevOps и CI/CD
  • Для студентов и новичков в веб-разработке, интересующихся автоматизацией процессов разработки

    Docker-compose в GitHub Actions — это не просто набор инструментов, а мощная комбинация для автоматизации сборки и деплоя многоконтейнерных приложений. Когда я впервые столкнулся с необходимостью запускать docker-compose в CI/CD пайплайнах, это казалось почти невозможной задачей из-за различий в окружениях и непредсказуемых проблем совместимости. Но правильная интеграция этих технологий способна радикально повысить эффективность разработки, обеспечивая полностью автоматизированное тестирование и деплой сложных приложений. Давайте разберемся, как настроить эту интеграцию правильно с первого раза. 🚀

Если вы хотите углубить свои знания в веб-разработке и DevOps, обратите внимание на обучение веб-разработке от Skypro. Курс включает не только базовые навыки программирования, но и практические модули по CI/CD, Docker и GitHub Actions — именно те инструменты, которые сделают вас востребованным специалистом, способным автоматизировать процессы разработки от начала до конца.

Зачем объединять docker-compose и GitHub Actions

Интеграция docker-compose в GitHub Actions решает одну из ключевых проблем современной разработки — обеспечение идентичности сред разработки, тестирования и продакшена. Когда разработчик говорит "у меня на локальной машине всё работает", а в продакшене система падает — это классический пример разрыва между средами выполнения.

Docker-compose в GitHub Actions позволяет:

  • Запускать многоконтейнерные приложения в CI/CD процессах точно так же, как на локальной машине
  • Тестировать взаимодействие между микросервисами в изолированной среде
  • Автоматизировать интеграционное тестирование с реальной базой данных и другими зависимостями
  • Создавать единообразные процессы сборки, которые одинаково работают у всех членов команды
  • Валидировать docker-compose конфигурации перед деплоем в продакшен

Алексей, DevOps-инженер

Я занимался внедрением CI/CD для стартапа, где приложение состояло из 5 взаимосвязанных микросервисов. Каждый разработчик использовал docker-compose на локальной машине, но тестирование при пулл-реквестах происходило в "чистой" среде, что приводило к постоянным проблемам.

Решением стала интеграция docker-compose в GitHub Actions. Мы настроили workflow, который поднимал все контейнеры в том же порядке и с теми же параметрами, что и локально. Это немедленно выявило несколько скрытых зависимостей, которые работали только на машинах разработчиков.

После внедрения количество "неожиданных" багов в деплойментах уменьшилось на 78%, а время, затрачиваемое на отладку проблем окружения, сократилось более чем втрое. Теперь, если тесты проходят в GitHub Actions, мы уверены, что они пройдут и в продакшене.

Давайте сравним подходы к тестированию в CI/CD пайплайнах:

Подход Преимущества Недостатки Применимость
Тестирование без контейнеризации Простота настройки, быстрый старт Проблемы совместимости, "it works on my machine" Малые проекты, монолитные приложения
Использование одиночных Docker контейнеров Изоляция, повторяемость Сложность настройки взаимодействия сервисов Средние проекты с одним сервисом
Docker-compose в GitHub Actions Полная изоляция, тестирование взаимодействия, идентичность сред Сложнее настройка, потребление ресурсов Микросервисная архитектура, сложные приложения

Объединение docker-compose и GitHub Actions — не просто дань моде на контейнеризацию. Это практический инструмент, который делает процесс CI/CD более надежным, особенно для сложных систем с множеством взаимодействующих компонентов. 🔄

Пошаговый план для смены профессии

Подготовка проекта для интеграции docker-compose

Прежде чем интегрировать docker-compose в GitHub Actions, необходимо правильно подготовить структуру проекта. Без этого этапа вы рискуете столкнуться с проблемами совместимости и неожиданным поведением контейнеров в CI/CD среде.

Основные шаги подготовки проекта:

  1. Оптимизация docker-compose.yml для CI/CD окружения
  2. Разделение конфигураций для разработки и тестирования
  3. Настройка параметров времени ожидания и проверок готовности сервисов
  4. Обеспечение идемпотентности процессов
  5. Минимизация размеров образов для ускорения CI/CD пайплайнов

Начнем с анализа существующего docker-compose.yml файла. В нем могут быть параметры, которые хорошо работают локально, но создадут проблемы в GitHub Actions. Например, монтирование локальных томов, использование специфичных для разработчика переменных окружения, или hardcoded пути к файлам.

Ключевые элементы, которые следует оптимизировать в docker-compose.yml:

yaml
Скопировать код
version: '3.8'

services:
app:
build:
context: ./app
# Используем ARG для переключения между development и CI
args:
- NODE_ENV=${NODE_ENV:-production}
environment:
# Используем переменные окружения из GitHub Actions
- DATABASE_URL=${DATABASE_URL}
# Задаем значения по умолчанию для локальной разработки
- PORT=${PORT:-3000}
# Настраиваем проверки готовности для CI/CD
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s

db:
image: postgres:14
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
# Вместо volumes используем инициализационные скрипты
volumes:
- ./init-db:/docker-entrypoint-initdb.d
# Для CI/CD избегаем постоянных томов
# – postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 5s
retries: 5

Часто бывает удобно создать отдельный docker-compose.ci.yml файл, который будет специально оптимизирован для CI/CD и переопределит некоторые параметры основного файла:

yaml
Скопировать код
version: '3.8'

services:
app:
build:
args:
- NODE_ENV=test
environment:
- CI=true
- TEST_DATABASE_URL=postgres://postgres:postgres@db:5432/test

db:
environment:
- POSTGRES_DB=test
# Используем фиксированные учетные данные для тестов
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
# В CI нам не нужны персистентные данные
volumes:
- ./init-db:/docker-entrypoint-initdb.d

Михаил, Lead DevOps-инженер

Мой опыт показал, что правильная подготовка docker-compose для GitHub Actions может сэкономить недели отладки в будущем. В одном из проектов мы мигрировали CI/CD из Jenkins в GitHub Actions и столкнулись с загадочными сбоями тестов, которые проходили локально, но стабильно падали в CI.

Проблема оказалась в том, что мы использовали volume для базы данных в docker-compose.yml без явного указания каталога. Локально это работало отлично — данные сохранялись между запусками. Но в GitHub Actions каждый запуск происходил в чистом окружении, и база данных каждый раз инициализировалась с нуля, что приводило к конфликтам в миграциях.

Мы решили проблему, создав два файла: docker-compose.yml для разработки и docker-compose.ci.yml для CI/CD, который переопределял настройки томов. Дополнительно мы добавили wait-for-it скрипт, гарантирующий, что тесты запускаются только когда все сервисы полностью готовы.

Этот подход сократил время выполнения CI/CD пайплайнов на 40% и полностью устранил "мерцающие" тесты, которые случайно проходили или не проходили из-за проблем с окружением.

Важный аспект подготовки — оптимизация размеров Docker-образов для CI/CD. Чем меньше образы, тем быстрее будут выполняться ваши workflow в GitHub Actions:

Техника оптимизации Описание Примерное уменьшение размера
Multi-stage builds Разделение этапа сборки и запуска в Dockerfile 50-80%
Минимальные базовые образы Использование alpine или slim вариантов образов 60-90%
Оптимизация слоев Объединение RUN команд, удаление временных файлов 10-30%
.dockerignore Исключение ненужных файлов из контекста сборки 5-50% (зависит от проекта)
Кэширование зависимостей Правильный порядок операций для максимального использования кэша Сокращение времени сборки на 30-70%

Подготовка проекта для интеграции docker-compose в GitHub Actions — не единоразовая задача, а непрерывный процесс. С каждой новой итерацией ваш CI/CD пайплайн должен становиться быстрее и надежнее. 🛠️

Создание YAML-файла workflow для запуска контейнеров

Ключевой элемент интеграции docker-compose в GitHub Actions — корректно настроенный workflow файл. Этот YAML документ определяет, когда и как будут запускаться ваши контейнеры, а также последовательность выполнения команд. Давайте разберем создание такого файла пошагово.

Для начала создайте в корне проекта директорию .github/workflows/ и файл внутри неё, например docker-compose-ci.yml. В этом файле мы определим workflow для запуска docker-compose в GitHub Actions.

Базовая структура workflow файла выглядит так:

yaml
Скопировать код
name: Docker Compose CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Docker Compose
uses: docker/setup-buildx-action@v1

# Здесь будут шаги для работы с docker-compose

Теперь добавим шаги для запуска и взаимодействия с docker-compose. Обратите внимание на оптимальную последовательность действий:

yaml
Скопировать код
name: Docker Compose CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Build containers
run: docker-compose build

- name: Start containers
run: docker-compose up -d

- name: Wait for services to be ready
run: |
# Ждем 10 секунд для инициализации сервисов
sleep 10
# Проверяем, что все сервисы запущены и здоровы
docker-compose ps

- name: Run tests
run: docker-compose exec -T app npm test

- name: Show logs on failure
if: failure()
run: docker-compose logs

- name: Stop containers
if: always()
run: docker-compose down

Для более сложных сценариев может потребоваться использование нескольких docker-compose файлов или дополнительных опций:

yaml
Скопировать код
name: Docker Compose Advanced CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Build and start containers
run: |
# Используем несколько конфигурационных файлов
docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d

- name: Wait for services to be healthy
run: |
# Более продвинутое ожидание готовности сервисов
MAX_RETRIES=30
RETRY_INTERVAL=5

for i in $(seq 1 $MAX_RETRIES); do
if docker-compose ps | grep -q "healthy"; then
echo "Services are healthy!"
break
fi

echo "Waiting for services to be healthy... ($i/$MAX_RETRIES)"
sleep $RETRY_INTERVAL

if [ $i -eq $MAX_RETRIES ]; then
echo "Services did not become healthy in time."
docker-compose ps
docker-compose logs
exit 1
fi
done

- name: Run integration tests
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml exec -T app npm run test:integration

- name: Run e2e tests
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml exec -T app npm run test:e2e

- name: Push images if tests passed
if: success() && github.ref == 'refs/heads/main'
run: |
docker-compose push app

- name: Cleanup
if: always()
run: |
docker-compose -f docker-compose.yml -f docker-compose.ci.yml down -v
docker system prune -f

Важные рекомендации при создании workflow файла для docker-compose в GitHub Actions:

  • Всегда используйте флаг -T при запуске команд с docker-compose exec, так как GitHub Actions работает в неинтерактивном режиме
  • Добавляйте шаг очистки ресурсов с условием if: always(), чтобы контейнеры были остановлены даже при ошибке
  • Выводите логи контейнеров при неудаче тестов для упрощения отладки
  • Используйте несколько docker-compose файлов для разделения конфигураций разработки и CI/CD
  • Добавляйте достаточное время ожидания и проверки готовности сервисов перед запуском тестов

Для сложных приложений может потребоваться более тонкая настройка параллельного выполнения задач в GitHub Actions. Рассмотрим пример с использованием матрицы для выполнения разных типов тестов:

yaml
Скопировать код
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-type: [unit, integration, e2e]
# Продолжать выполнение других тестов даже если один тип упал
fail-fast: false
steps:
- uses: actions/checkout@v2

- name: Build and start containers
run: |
docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d

- name: Run ${{ matrix.test-type }} tests
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml exec -T app npm run test:${{ matrix.test-type }}

- name: Cleanup
if: always()
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml down -v

Кэширование Docker образов и слоев существенно ускоряет выполнение workflow. Для этого можно использовать GitHub Actions cache:

yaml
Скопировать код
steps:
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Build and load
uses: docker/build-push-action@v2
with:
context: ./app
load: true
tags: myapp:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new

# Костыль для обхода ограничения GitHub Actions cache
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

Создание эффективного YAML-файла workflow требует понимания не только docker-compose, но и особенностей работы GitHub Actions. Постепенно улучшайте ваш workflow, добавляя кэширование, параллельное выполнение тестов и другие оптимизации. 🔧

Настройка секретов и переменных окружения для docker-compose

Безопасная работа с секретами и переменными окружения — критически важный аспект интеграции docker-compose в GitHub Actions. Неправильная настройка может привести как к утечке конфиденциальных данных, так и к неработоспособности ваших контейнеров в CI/CD окружении.

GitHub Actions предоставляет два основных механизма для работы с конфиденциальными данными:

  • Secrets — зашифрованные переменные, безопасные для хранения чувствительной информации
  • Environment Variables — обычные переменные для параметризации workflow

Рассмотрим процесс настройки секретов для использования в docker-compose:

  1. Перейдите в настройки вашего репозитория на GitHub: Repository → Settings → Secrets → New repository secret
  2. Создайте необходимые секреты, например: DATABASEPASSWORD, APIKEY, JWT_SECRET
  3. Обновите ваш workflow файл, чтобы передать секреты в docker-compose
yaml
Скопировать код
name: Docker Compose CI with Secrets

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

# Создаем файл .env с секретами для docker-compose
- name: Create .env file
run: |
echo "DATABASE_URL=postgresql://user:${{ secrets.DB_PASSWORD }}@postgres:5432/mydb" > .env
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env

# Запускаем docker-compose с переменными окружения из .env
- name: Start services
run: docker-compose up -d

# Для команд, требующих переменные окружения напрямую
- name: Run migrations
env:
DATABASE_URL: postgresql://user:${{ secrets.DB_PASSWORD }}@postgres:5432/mydb
run: docker-compose exec -T app npm run migrate

Для передачи переменных окружения напрямую в docker-compose, можно использовать один из следующих подходов:

yaml
Скопировать код
# Передача через env в workflow
- name: Start services with env variables
env:
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
run: docker-compose up -d

# Или с использованием аргументов command line
- name: Start services with command line args
run: |
DATABASE_PASSWORD=${{ secrets.DB_PASSWORD }} \
API_KEY=${{ secrets.API_KEY }} \
docker-compose up -d

Хорошей практикой является создание отдельной GitHub Actions среды (environment) для разных стадий разработки:

yaml
Скопировать код
name: Docker Compose Multi-Environment CI

on:
push:
branches:
- main
- develop

jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v2

- name: Create environment-specific .env file
run: |
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" > .env
echo "ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}" >> .env

- name: Start services
run: docker-compose -f docker-compose.yml -f docker-compose.${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}.yml up -d

При работе с секретами и переменными окружения в docker-compose важно помнить о следующих практиках безопасности:

  • Никогда не выводите секреты в логи — GitHub автоматически скрывает секреты, но не если вы намеренно их выводите
  • Не сохраняйте секреты в файлы, которые могут быть зафиксированы в репозитории
  • Удаляйте файлы .env после использования
  • Используйте минимально необходимые привилегии для контейнеров
  • Регулярно обновляйте и ротируйте секреты

Для сложных проектов с множеством переменных окружения удобно использовать матрицу переменных:

Среда Переменные окружения Файл docker-compose
Development NODE_ENV=development, DEBUG=true docker-compose.yml + docker-compose.dev.yml
Testing (CI) NODE_ENV=test, CI=true docker-compose.yml + docker-compose.ci.yml
Staging NODEENV=staging, APIURL=staging-api docker-compose.yml + docker-compose.staging.yml
Production NODEENV=production, HIGHAVAILABILITY=true docker-compose.yml + docker-compose.prod.yml

При работе с переменными окружения для docker-compose в GitHub Actions обращайте внимание на область видимости переменных. Если они нужны только внутри контейнера, указывайте их в docker-compose файле. Если они нужны для принятия решений в workflow, используйте env в шагах GitHub Actions.

Настройка секретов и переменных окружения для docker-compose — это баланс между удобством разработки и безопасностью. Тщательно продуманная стратегия управления конфигурациями сделает ваш CI/CD процесс более надежным и безопасным. 🔐

Решение проблем совместимости docker-compose в GitHub Actions

Интеграция docker-compose в GitHub Actions часто сопровождается непредвиденными проблемами совместимости, которые могут существенно затруднить настройку CI/CD пайплайна. Рассмотрим наиболее распространенные проблемы и их решения.

Основные категории проблем совместимости:

  • Различия в версиях docker и docker-compose
  • Проблемы с сетевыми настройками и DNS-разрешением
  • Ограничения ресурсов и тайм-ауты в GitHub Actions
  • Особенности файловой системы и прав доступа
  • Различия в поведении контейнеров в разных окружениях

Проблема 1: Несовместимость версий

GitHub Actions может использовать версию docker-compose, отличающуюся от вашей локальной среды, что приводит к неожиданному поведению.

Решение:

yaml
Скопировать код
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Install specific docker-compose version
run: |
sudo rm /usr/local/bin/docker-compose
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o docker-compose
chmod +x docker-compose
sudo mv docker-compose /usr/local/bin
docker-compose --version

Проблема 2: Тайм-ауты при инициализации сервисов

В GitHub Actions сервисы могут запускаться дольше, чем на локальной машине, что приводит к преждевременному запуску тестов до полной готовности всех компонентов.

Решение — использовать скрипт ожидания:

yaml
Скопировать код
- name: Wait for services
run: |
# Сохраняем скрипт wait-for-it.sh
curl -L https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -o wait-for-it.sh
chmod +x wait-for-it.sh

# Ожидаем готовности необходимых сервисов
./wait-for-it.sh db:5432 -t 60
./wait-for-it.sh redis:6379 -t 60
./wait-for-it.sh app:3000 -t 120

Проблема 3: Проблемы с Docker сетью

В GitHub Actions могут возникать проблемы с DNS-разрешением или сетевыми подключениями между контейнерами.

Решение:

yaml
Скопировать код
- name: Debug network
run: |
# Проверяем сети Docker
docker network ls

# Получаем информацию о сети нашего приложения
docker network inspect docker_default

# Проверяем DNS-разрешение из контейнера
docker-compose exec -T app ping -c 3 db
docker-compose exec -T app cat /etc/hosts

Проблема 4: Ограничения ресурсов

GitHub Actions имеет ограничения по CPU и памяти, что может приводить к неожиданным сбоям при высокой нагрузке.

Решение — оптимизация ресурсов в docker-compose.ci.yml:

yaml
Скопировать код
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M

db:
deploy:
resources:
limits:
memory: 256M

Проблема 5: Зависание или "зомби" контейнеры

Иногда контейнеры могут "застревать" и не завершаться корректно, блокируя последующие запуски.

Решение — добавьте шаг очистки с принудительным удалением:

yaml
Скопировать код
- name: Cleanup containers
if: always()
run: |
docker-compose down -v --remove-orphans
# Удаляем все контейнеры и образы для полной очистки
docker ps -aq | xargs -r docker rm -f
docker system prune -af --volumes

Сравнительная таблица проблем совместимости и их решений:

Проблема Симптомы Решение Превентивные меры
Несовместимость версий Ошибки синтаксиса, неподдерживаемые опции Явно установить требуемую версию Указать версию в docker-compose.yml
Тайм-ауты сервисов Тесты падают из-за недоступности сервисов Использовать скрипты ожидания, healthcheck Добавить healthcheck во все сервисы
Сетевые проблемы Сервисы не видят друг друга, DNS ошибки Отладка сети, явное указание hostname Использовать фиксированные имена сетей
Ограничения ресурсов OOM ошибки, неожиданное завершение Ограничить ресурсы в docker-compose Мониторить использование ресурсов
"Зомби" контейнеры Новые запуски не могут стартовать Принудительная очистка контейнеров Всегда использовать if: always() для cleanup
Проблемы с томами Ошибки прав доступа, отсутствие данных Использовать временные тома или bind mounts Тестировать в CI с теми же настройками томов

Для систематического выявления и решения проблем совместимости полезно добавить отладочный step в workflow:

yaml
Скопировать код
- name: Debug information
if: always()
run: |
echo "=== Docker version ==="
docker --version
docker-compose --version

echo "=== Running containers ==="
docker ps -a

echo "=== Docker networks ==="
docker network ls

echo "=== Docker volumes ==="
docker volume ls

echo "=== Disk space ==="
df -h

echo "=== Memory usage ==="
free -h

echo "=== Container logs ==="
docker-compose logs

Проблемы совместимости docker-compose в GitHub Actions — это не разовые препятствия, а системные вызовы, требующие стратегического подхода. Разработайте практики, которые позволят своевременно выявлять и устранять эти проблемы, что существенно повысит надежность ваших CI/CD пайплайнов. 🛡️

Docker-compose в GitHub Actions — это действенный инструмент для создания надежных и воспроизводимых CI/CD пайплайнов. Правильная интеграция этих технологий значительно снижает разрыв между средами разработки и продакшена, минимизируя печально известное "на моей машине работает". Преимущества этого подхода становятся особенно очевидны при работе с микросервисной архитектурой, когда тестирование взаимодействия между компонентами становится критически важным. Применяйте описанные техники последовательно, уделяя внимание безопасности и оптимизации производительности, и вы получите CI/CD процесс, который вызовет восхищение у ваших коллег и уважение у руководства.

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

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой файл содержит конфигурацию для всех сервисов в Docker Compose?
1 / 5

Загрузка...