Docker для QA: контейнеры без боли

Всё, что QA должен знать о Docker — без лишнего DevOps, с реальными задачами


Docker для QA

Зачем QA вообще Docker?

Потому что «у меня всё работает» — это не баг-репорт, а крик о помощи.

Вот реальная ситуация. Тестировщик прогоняет автотесты на своей машине — всё зелёное. Пушит в CI — три теста красные. Начинает разбираться: на его машине PostgreSQL 15, а в CI — PostgreSQL 14. На его машине Node.js 20, в CI — 18. На его машине Redis запущен, в CI — нет. Потратил полдня на отладку окружения вместо тестирования.

Если бы тесты запускались в Docker — проблемы бы не было. Контейнер одинаковый везде: на ноутбуке, в CI, у коллеги. Это и есть главная идея.

Docker нужен QA для четырёх вещей:

#ЗачемПример
1Одинаковое окружениеЛокально, в CI, у коллеги — одна и та же версия всего
2Быстрый старт зависимостейНужна БД для тестов? docker run postgres — 5 секунд
3ИзоляцияТест-сьют не ломает вашу систему, а контейнер можно удалить без следа
4Воспроизведение багов«Баг только на production» — подними такой же контейнер и проверь

Что такое Docker (простыми словами)

Представьте, что вы заказали пиццу. Вам привезли коробку, внутри — готовая пицца. Вам не нужно знать, какая печь на кухне, какая марка муки, какой рецепт теста. Вы открываете коробку — и едите.

Docker работает так же. Вместо того чтобы устанавливать PostgreSQL, Node.js, Redis и ещё 15 зависимостей на свою машину — вы запускаете контейнер, в котором всё это уже есть и настроено. Контейнер — это коробка с готовым приложением.

Три ключевых понятия:

  • Image (образ) — рецепт. Описание того, что внутри контейнера. Скачивается один раз.
  • Container (контейнер) — работающий экземпляр образа. Как запущенная программа.
  • Dockerfile — инструкция для создания образа. Текстовый файл с командами.

Аналогия: образ — это класс, контейнер — это объект (экземпляр класса). Из одного образа можно запустить 10 контейнеров.


Установка Docker

Windows

Скачайте Docker Desktop и установите. После установки в трее появится иконка кита. Если кит счастливый — Docker работает.

macOS

Тоже Docker Desktop. Скачать → перетащить в Applications → запустить.

Linux

sudo apt-get update
sudo apt-get install docker.io docker-compose
sudo usermod -aG docker $USER

После установки перезайдите в систему и проверьте:

docker --version

Если видите что-то вроде Docker version 27.x.x — всё работает.


docker run — запускаем первый контейнер

Начнём с простого. Запустим PostgreSQL:

docker run --name test-db -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:16

Разберём по кусочкам:

ЧастьЧто делает
docker runСоздать и запустить контейнер
--name test-dbДать контейнеру имя (чтобы не запоминать ID)
-e POSTGRES_PASSWORD=secretПеременная окружения (пароль для БД)
-p 5432:5432Пробросить порт: порт_хоста:порт_контейнера
-dЗапустить в фоне (detached)
postgres:16Образ и версия

Через 5 секунд у вас работает PostgreSQL. Подключитесь к нему через DBeaver, pgAdmin или из тестов — localhost:5432, пользователь postgres, пароль secret.

Ловушка: -p 5432:5432 — порт слева это порт на вашей машине. Если 5432 уже занят (у вас локально стоит Postgres), используйте -p 5433:5432. Подключаетесь к localhost:5433, а внутри контейнера всё равно 5432.


Основные команды Docker

Вот минимум, который нужно знать:

Жизненный цикл контейнера

docker run -d --name my-app nginx
docker ps
docker stop my-app
docker start my-app
docker restart my-app
docker rm my-app
docker rm -f my-app
КомандаЧто делает
docker runСоздать + запустить контейнер
docker psСписок запущенных контейнеров
docker ps -aСписок всех контейнеров (включая остановленные)
docker stopОстановить контейнер
docker startЗапустить остановленный контейнер
docker rmУдалить контейнер
docker rm -fПринудительно удалить (даже запущенный)

Работа с образами

docker images
docker pull nginx:latest
docker rmi nginx:latest
КомандаЧто делает
docker imagesСписок скачанных образов
docker pullСкачать образ
docker rmiУдалить образ

Отладка

docker logs my-app
docker logs -f my-app
docker exec -it my-app bash
docker inspect my-app
КомандаЧто делает
docker logsПосмотреть логи контейнера
docker logs -fСледить за логами в реальном времени (как tail -f)
docker exec -it ... bashЗайти внутрь контейнера
docker inspectПодробная информация о контейнере (сеть, порты, переменные)

Задача с собеседования: «Тесты на CI падают с ошибкой connection refused к базе. Как дебажить?»

Ответ: docker ps — убедиться, что контейнер с БД запущен. docker logs <имя> — проверить, нет ли ошибок при старте. docker inspect <имя> — проверить, на каком порту и в какой сети контейнер. Частая причина: контейнер с приложением и контейнер с БД в разных Docker-сетях.


Dockerfile — создаём свой образ

Для QA это актуально, когда нужно собрать образ с тестами. Например, упаковать Selenium-тесты в контейнер, чтобы запускать их в CI.

Структура

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["pytest", "--tb=short", "-v"]

Разбор:

ИнструкцияЧто делает
FROMБазовый образ (на чём строим)
WORKDIRРабочая директория внутри контейнера
COPYКопировать файлы с хоста в контейнер
RUNВыполнить команду при сборке образа
CMDКоманда по умолчанию при запуске контейнера

Собираем и запускаем

docker build -t my-tests .
docker run my-tests

-t my-tests — даём образу тег (имя). Точка . — контекст сборки (текущая папка, где лежит Dockerfile).

Частые ошибки в Dockerfile

1. COPY перед установкой зависимостей:

# ❌ Плохо — каждое изменение кода пересобирает зависимости
COPY . .
RUN pip install -r requirements.txt

# ✅ Хорошо — зависимости кэшируются
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Docker кэширует слои. Если requirements.txt не менялся — pip install не будет выполняться заново. Но если сначала скопировать весь код, любое изменение в одном файле инвалидирует кэш.

2. Использование latest вместо конкретной версии:

# ❌ Сегодня Python 3.12, завтра 3.13 — тесты сломались
FROM python:latest

# ✅ Фиксированная версия
FROM python:3.12-slim

Docker Compose — поднимаем окружение целиком

docker run — это один контейнер. Но для тестирования обычно нужно несколько: база, API, кэш, очередь. Запускать каждый руками — мучение. Docker Compose позволяет описать всё в одном файле и поднять одной командой.

Пример: API + PostgreSQL + Redis

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: tester
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tester -d testdb"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  api:
    image: myapp/api:latest
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgresql://tester:secret@db:5432/testdb
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

Обратите внимание на несколько вещей:

  1. db, redis, api — имена сервисов. Они же работают как DNS-имена внутри Docker-сети. API обращается к базе по адресу db:5432, а не localhost:5432.

  2. depends_on — порядок запуска. API запустится только после того, как БД станет healthy (пройдёт healthcheck).

  3. healthcheck — проверка, что сервис действительно готов принимать соединения. Без него depends_on просто ждёт старта контейнера, а не готовности сервиса.

Команды Docker Compose

docker compose up -d
docker compose ps
docker compose logs api
docker compose logs -f
docker compose down
docker compose down -v
КомандаЧто делает
up -dПоднять всё в фоне
psСтатус всех сервисов
logs <сервис>Логи конкретного сервиса
downОстановить и удалить контейнеры
down -vТо же + удалить volumes (данные)

Ловушка: docker compose down не удаляет volumes. Если тесты записали мусор в БД — при следующем up данные останутся. Используйте down -v для чистого старта.


Volumes — данные, которые переживут контейнер

Контейнер — штука одноразовая. Удалили контейнер — данные внутри пропали. Volumes решают эту проблему.

services:
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

volumes:
  db-data:

Здесь две разных привязки:

  • db-data:/var/lib/... — именованный volume. Docker управляет им сам. Данные БД сохраняются между перезапусками.
  • ./init.sql:/docker-entrypoint-initdb.d/init.sql — bind mount. Файл с хоста монтируется внутрь контейнера. PostgreSQL автоматически выполнит SQL-файлы из docker-entrypoint-initdb.d при первом запуске.

Когда что использовать:

ТипКогда
Named volumeДанные БД, кэш — то, что Docker управляет сам
Bind mountКонфиги, init-скрипты, исходный код — то, что вы редактируете на хосте

Docker для тестов: практические сценарии

Сценарий 1: Тестовая БД с начальными данными

Создайте файл init.sql:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

INSERT INTO users (name, email) VALUES
    ('Atajan', 'atajan@test.com'),
    ('Maria', 'maria@test.com'),
    ('John', 'john@test.com');

И docker-compose.yml:

services:
  test-db:
    image: postgres:16
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: tester
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
docker compose up -d

Через 5 секунд у вас БД с тестовыми данными. Подключаетесь из тестов к localhost:5432 — и работаете. После тестов — docker compose down -v — и всё чисто.

Сценарий 2: Selenium-тесты в контейнере

services:
  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - "4444:4444"
      - "7900:7900"
    shm_size: "2g"

  tests:
    build: .
    depends_on:
      - chrome
    environment:
      SELENIUM_URL: http://chrome:4444/wd/hub

shm_size: "2g" — важный нюанс. Chrome в контейнере использует shared memory, и стандартных 64MB не хватает. Без этого параметра тесты будут падать с непонятными ошибками. Порт 7900 — VNC, можно подключиться и наблюдать, что делает браузер.

Сценарий 3: Полный тестовый стенд

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5

  api:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      API_URL: http://api:8080

  tests:
    build: ./tests
    depends_on:
      - api
      - frontend
    environment:
      BASE_URL: http://frontend:3000
      API_URL: http://api:8080

Одна команда docker compose up --abort-on-container-exit tests — и весь стенд поднимается, тесты прогоняются, всё останавливается. Флаг --abort-on-container-exit завершает все сервисы, когда контейнер tests завершится.


Networking — почему контейнеры (не) видят друг друга

Это самый частый источник проблем. Разберёмся раз и навсегда.

Правило 1: Контейнеры в одном docker-compose.yml видят друг друга по имени сервиса. API обращается к базе по адресу db:5432, не localhost:5432.

Правило 2: localhost внутри контейнера — это сам контейнер, не ваша машина. Если API в контейнере обращается к localhost:5432 — он ищет Postgres внутри себя, а не в соседнем контейнере.

Правило 3: Порты через -p нужны только для доступа с хоста. Контейнеры между собой общаются по внутренней сети без проброса портов.

┌─────────────────────────────────────────┐
│              Docker Network             │
│                                         │
│  ┌─────────┐         ┌─────────┐       │
│  │   api   │───5432──│   db    │       │
│  │  :8080  │         │  :5432  │       │
│  └────┬────┘         └─────────┘       │
│       │                                 │
└───────┼─────────────────────────────────┘
        │ -p 8080:8080

   ┌────┴────┐
   │  Host   │  ← ваш ноутбук
   │  :8080  │
   └─────────┘

Задача с собеседования: «Тесты подключаются к БД по localhost:5432, но получают connection refused. Контейнер с БД запущен. В чём проблема?»

Ответ: если тесты тоже в контейнере — нужно использовать имя сервиса (db:5432), а не localhost. Если тесты на хосте — нужно убедиться, что порт проброшен (-p 5432:5432).


Переменные окружения и .env

Не хардкодьте пароли и URL-ы в docker-compose.yml. Используйте .env:

# .env
POSTGRES_USER=tester
POSTGRES_PASSWORD=secret
POSTGRES_DB=testdb
API_PORT=8080
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - "${DB_PORT:-5432}:5432"

${DB_PORT:-5432} — если DB_PORT не задан, используется 5432. Удобно для переопределения на CI.

Docker Compose автоматически подхватывает файл .env из текущей директории. Не нужно ничего дополнительно указывать.


Docker в CI/CD

На CI Docker используется постоянно. Типичный пайплайн:

# .github/workflows/tests.yml (GitHub Actions)
name: Tests
on: [push, pull_request]

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

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

      - name: Wait for DB
        run: |
          until docker compose exec db pg_isready -U tester; do
            sleep 2
          done

      - name: Run tests
        run: docker compose run tests

      - name: Cleanup
        if: always()
        run: docker compose down -v

Ключевые моменты:

  • if: always() в cleanup — контейнеры будут остановлены даже если тесты упали
  • Ожидание готовности БД — depends_on не гарантирует, что Postgres принимает соединения
  • docker compose run tests запускает одноразовый контейнер — отработал и остановился

Задачи с реальных собеседований

Задача 1: «У меня локально работает, а в CI нет»

Тесты используют API на localhost:8080. Локально всё работает. В CI (GitHub Actions) — connection refused. Docker Compose одинаковый. В чём может быть проблема?

Ответ: локально тесты запускаются на хосте и обращаются к порту, проброшенному через -p. В CI тесты, скорее всего, запускаются в контейнере — нужно использовать имя сервиса (api:8080) вместо localhost:8080. Либо запускать тесты через docker compose run, где сеть общая.

Задача 2: «Тесты влияют друг на друга»

Тесты по отдельности зелёные, а вместе — падают. Один тест создаёт пользователя с email test@test.com, другой тоже. При последовательном запуске второй ловит unique constraint violation. Как исправить с помощью Docker?

Ответ: поднимать чистую БД перед каждым набором тестов. docker compose down -v && docker compose up -d перед запуском. Или использовать транзакции — каждый тест оборачивается в транзакцию и делает ROLLBACK в конце. Docker-решение более «грубое», но надёжное.

Задача 3: «Контейнер стартует, но сервис не отвечает»

docker compose up -d прошёл успешно, docker compose ps показывает все контейнеры running. Но API возвращает 502. Как дебажить?

Ответ:

  1. docker compose logs api — посмотреть логи API
  2. docker compose logs db — может, БД не запустилась
  3. docker compose exec api curl localhost:8080/health — проверить, что API отвечает изнутри
  4. Проверить healthcheck — контейнер может быть running, но не healthy
  5. docker inspect — проверить сеть и порты

Задача 4: «Объясни, что делает этот docker-compose.yml»

Интервьюер показывает файл и просит объяснить каждую строку. Частый формат на собеседованиях — проверяют, понимаете ли вы реально или заучили команды.

Для этого нужно понимать:

  • Что такое services, volumes, networks
  • Зачем depends_on и его ограничения
  • Разницу между build и image
  • Что делает healthcheck
  • Зачем -v в docker compose down

Задача 5: «Напиши docker-compose.yml для тестового окружения»

Нужно: PostgreSQL, Redis, бэкенд (образ myapp/api:v2), запуск тестов (папка ./tests, Dockerfile уже есть). Тесты должны запускаться после готовности API и БД.

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: tester
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tester -d testdb"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  api:
    image: myapp/api:v2
    environment:
      DATABASE_URL: postgresql://tester:secret@db:5432/testdb
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 5

  tests:
    build: ./tests
    environment:
      API_URL: http://api:8080
      DB_URL: postgresql://tester:secret@db:5432/testdb
    depends_on:
      api:
        condition: service_healthy

5 ловушек, на которых валятся кандидаты

1. localhost внутри контейнера ≠ ваша машина.

Внутри контейнера localhost — это сам контейнер. Чтобы достучаться до соседнего контейнера, используйте имя сервиса. Чтобы достучаться до хоста — host.docker.internal (Docker Desktop) или --network host.

2. depends_on не ждёт готовности сервиса.

depends_on ждёт только запуска контейнера, а не готовности приложения внутри. PostgreSQL может стартовать 10 секунд, а depends_on отпустит зависимый контейнер сразу. Используйте condition: service_healthy + healthcheck.

3. Данные в контейнере — временные.

Удалили контейнер — потеряли данные. Для сохранения используйте volumes. Для тестов это обычно плюс (чистый старт), но для тестовых БД с миграциями — нужно понимать.

4. Разница между docker compose down и docker compose down -v.

Без -v volumes сохраняются. С -v — удаляются. Если тесты засорили БД, а вы забыли -v — следующий прогон может упасть на грязных данных.

5. Разница между CMD и ENTRYPOINT в Dockerfile.

CMD — можно переопределить при docker run. ENTRYPOINT — нет (точнее, сложнее). Для тестов обычно CMD — чтобы можно было запустить конкретный тест: docker run my-tests pytest tests/test_login.py.


Чек-лист: что знать перед собеседованием

  • Концепции: image vs container, Dockerfile, Docker Compose
  • Команды: run, ps, stop, rm, logs, exec, build
  • Dockerfile: FROM, COPY, RUN, CMD, WORKDIR — и порядок слоёв для кэширования
  • Docker Compose: services, ports, environment, volumes, depends_on, healthcheck
  • Networking: почему localhost не работает между контейнерами, имена сервисов как DNS
  • Volumes: named volumes vs bind mounts, зачем -v в down
  • CI/CD: как Docker решает проблему «у меня работает», ожидание готовности сервисов
  • Отладка: logs, exec, inspect — три главных инструмента

Этого хватит для 90% QA-собеседований. Не нужно знать Kubernetes, Docker Swarm или multi-stage builds — это уже DevOps-территория.


Полезные ресурсы

  1. Docker Getting Started — официальный туториал. Хорошо структурирован, с примерами.
  2. Play with Docker — браузерная песочница. Не нужно ничего устанавливать — Docker прямо в браузере.
  3. Docker Compose documentation — справочник по docker-compose.yml.
  4. Awesome Docker — курированный список ресурсов.

Курс по тестированию с практическими заданиями — бесплатно на annayev.com (English, Русский, Türkçe).

Ставьте плюс, если было полезно. Используете Docker в повседневной работе тестировщика? Расскажите в комментариях, какие сценарии у вас самые частые.