---
title: "Ryuk и уборка мусора"
description: "Кто убирает мусор в testcontainers: уборщик Ryuk и метки сессии, что стирается, RYUK_DISABLED для CI и отладки, утечки после падений и ручная гигиена Docker."
course: "Testcontainers в Go: интеграционные тесты с Docker"
url: "https://golangmaster.ru/courses/testcontainers/lessons/18-ryuk-i-resursy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Ryuk и уборка мусора

*Курс «Testcontainers в Go: интеграционные тесты с Docker», урок 18 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/testcontainers/lessons/18-ryuk-i-resursy/*

Половина уроков курса заканчивалась одной фразой: Terminate в t.Cleanup — и мусора не будет. Пора спросить, кто уберёт, если Cleanup не исполнится: процесс теста убит, пайплайн снят посреди прогона, ноутбук уснул вместе с Docker. Сценарий «testcontainers ryuk» — про этот случай: reaper docker-уборщик, которого библиотека заводит сама, кормит метками и оставляет дежурить даже после смерти процесса. Разберём, как он устроен и почему у него два таймаута, что именно попадает под метки, когда его выключают через RYUK_DISABLED и какая гигиена остаётся на вас, чтобы локальная машина и CI не зарастали зомби.

Уборка в testcontainers — двухслойная, и путать слои дорого. Первый — ваш собственный: Terminate, зарегистрированный в t.Cleanup, исполняется при провале, панике и штатном выходе — механика разобрана в [уроке о жизненном цикле](/courses/testcontainers/lessons/03-zhiznennyj-cikl/). Этот слой быстрый и предсказуемый: ресурс освобождается мгновенно, не дожидаясь конца сессии. Второй слой — страховочный: он не знает про тесты и Cleanup, он знает про процесс. Пока go test жив, страховка молчит; умрёт процесс вместе со всеми незакрытыми Cleanup — страховка сработает и выметет всё, что процесс насоздавал. Архитектура закрывает дыру, которую не закрыть изнутри: ни один defer не выполнится в процессе, убитом SIGKILL'ом. Отсюда правило чтения урока: первый слой — дисциплина, второй — гарантия. Убрать первый — второй не даст утонуть окружению, но потянет до конца сессии с лишними пожирателями памяти и портов. Убрать второй — останется одна дисциплина, и единственная забытая уборка превращается в зомби, который живёт до ручной зачистки. Коварство утечек в том, что на быстрой машине они невидимы: пара лишних контейнеров ничего не ломает, тесты зелёные, и кажется, что всё под контролем. Рокировка происходит на перегруженном CI: там же исчерпаются свободные порты, кончится место под образы, а соседний прогон начнёт ловить странные отказы в подключении. Урок 18 — о том, кому доверена уборка, когда вы закрываете ноутбук посреди прогона.

### Reaper docker-уборщик: как он устроен

Связка «testcontainers ryuk» всплывает в багтрекерах не зря: без понимания механики половина поведения уборщика выглядит мистикой. При первом же контейнере библиотека заводит sidecar — контейнер из образа testcontainers/ryuk с именем reaper_<sessionID>, где sessionID — случайный идентификатор сессии. Ему монтируют docker.sock — без него убирать чужие ресурсы нечем, — ставят AutoRemove, чтобы он вычистил и сам себя, и ждут старта по журналу и порту: комбинированная стратегия ждёт строчку Started в логах и открытый порт 8080/tcp, который при желании сдвигается переменной RYUK_PORT. Дальше рукопожатие: тестовый процесс подключается к уборщику по TCP и передаёт набор фильтров по меткам сессии, тот отвечает ACK, и соединение считается рабочим. Уборщик — синглтон: второй контейнер не плодит второго уборщика, библиотека находит живого по меткам и имени и переиспользует. Старт не мгновенный: если уборщика нет, его находят или создают с ретраями и растущими паузами — на всё про всё около двадцати секунд, поэтому первый тест в прогоне просыпается чуть медленнее остальных. Пока тесты идут, создание каждого ресурса держит собственное соединение с уборщиком. Стоит процессу умереть — обрываются все разом. Рукопожатие — не формальность: ошибка подключения не глотается тихо, а после исчерпания попыток всплывает из создания контейнера на руки тесту, так что сломанный уборщик невозможно перепутать с медленным демоном.

### Два слоя защиты в одном тесте

```go
func TestPayments(t *testing.T) {
	ctx := context.Background()

	ctr, err := redis.Run(ctx, "redis:7-alpine")
	if err != nil {
		t.Fatalf("redis: %v", err)
	}
	// слой 1: сработает при провале, панике и штатном выходе
	t.Cleanup(func() { _ = ctr.Terminate(ctx) })

	// слой 2 уже дежурит: reaper_<sessionID> держит соединение
	// и видит смерть процесса там, где Cleanup бессилен

	addr, err := ctr.Endpoint(ctx, "")
	if err != nil {
		t.Fatalf("endpoint: %v", err)
	}
	_ = addr // ... работа с сервисом
}
```

Слои не конкурируют: Cleanup отвечает за штатные и полуштатные исходы, уборщик — за смерть процесса. Endpoint забирают после Run: модуль сам дождался готовности. Закомментируйте Cleanup — тест всё равно оставит чистую машину, но ресурс будет жечь память до конца сессии. Выключите уборщика — останется одна дисциплина: единственная забытая уборка станет зомби.

### Что попадает под метки: контейнеры, сети и тома

Уборщик не гадает, что удалять: он сверяется с метками, а библиотека снабжает ими всё, что создаёт. Служебный набор скромен: org.testcontainers=true, язык go, версия библиотеки, идентификатор сессии и — главный — org.testcontainers.reap, разрешение на зачистку. Разрешение ставится только при включённом уборщике: выключите Ryuk — меток reap на ресурсах не будет вовсе, и автозачистка теряет право голоса. Маркировка достаётся и сетям: при создании библиотека дописывает тот же набор, поэтому сеть-сирота не переживёт сессию. Меченым оказывается вообще всё, что выходит из провайдера, — включая вспомогательные контейнеры вроде sshd-проброса портов, который библиотека заводит для доступа к локальным службам изнутри контейнера. Пространство имён org.testcontainers — служебное: метку с таким префиксом библиотека отклонит с ошибкой, для своих пометок выбирайте собственный префикс. Что стирается по меткам: контейнеры, сети, тома. С томами тонкость: force-remove при Terminate сносит анонимные тома контейнера, а именованные переживают удаление — их перечисляют опцией RemoveVolumes или подметают prune'ом с фильтром по меткам. Наконец, уборщик прибирает и себя: AutoRemove срабатывает, когда зачищать больше нечего, — от sidecar не остаётся даже имени.

### Проверка: кто приберёт после SIGKILL

**Вопрос:** Процесс go test убит kill -9. t.Cleanup не выполнился. Кто и когда приберёт контейнеры теста?

- Никто: без Cleanup контейнеры останутся на демоне навсегда
- ✅ Reaper: обрыв соединения, пауза reconnection timeout, зачистка ресурсов по меткам сессии
- AutoRemove: библиотека включает его для каждого тестового контейнера
- Docker сам заметит осиротевшие контейнеры и удалит их по истечении часа

AutoRemove в библиотеке стоит только у самого reaper-контейнера, а у Docker нет часовых таймеров на сирот. Cleanup при SIGKILL не исполняется — это и есть дыра, которую закрывает уборщик: соединения обрываются, истекает reconnection timeout, и ресурсы сессии стираются по меткам, включая сети и тома.

### TESTCONTAINERS_RYUK_DISABLED: когда выключение оправдано

Выключатель один, сценариев два. Первый — переменная TESTCONTAINERS_RYUK_DISABLED=true: окружение сильнее файла, поэтому в CI дёргают именно её, и сравнение здесь строгое — выключит только литеральное true, единица или tRuE уборщика не остановят. Второй — свойство ryuk.disabled=true в ~/.testcontainers.properties: удобно включать флаг локально и надолго. Что происходит после: reaper-контейнер не стартует вообще, рукопожатий нет, метки reap не раздаются, автозачистки нет, старт первого теста чуть ускоряется — минус один контейнер в пачке. Оправданий ровно два. Первое — CI, где окружение одноразовое и уборку ведёт сам раннер: гонять уборщика в среде, которую снесут через минуту, — работа без клиента. Второе — отладка: выключенный уборщик оставляет контейнер живым после упавшего теста, и docker logs показывает всё, что нужно. Отдельная история — окружения, где уборщику не дают подняться: если демон отказывает в запуске, сначала проверьте флаг TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true и только потом думайте о глушении. Тонкие настройки живут в соседних переменных: RYUK_CONNECTION_TIMEOUT — таймаут на подключение, минута по умолчанию; RYUK_RECONNECTION_TIMEOUT — сколько ждать пропавшего клиента, десять секунд; RYUK_VERBOSE — болтливые логи уборщика.

### Выключение и флаг привилегий

```go
# CI без права на привилегированные контейнеры:
# уборку берёт на себя одноразовый раннер
export TESTCONTAINERS_RYUK_DISABLED=true
go test ./...

# отладка упавшего теста: контейнер останется живым для docker logs
TESTCONTAINERS_RYUK_DISABLED=true go test -run TestBroken -count=1

# если окружение ругается на запуск уборщика:
export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true
```

Первые два вызова — про выключение: env-переменная перекрывает properties-файл, поэтому в пайплайнах ставят её, а локально удобнее флаг в файле. Третий вызов — про окружения, где уборщику не хватает прав: privileged-режим решает, глушить Ryuk не требуется. Отладочный прогон с выключенным уборщиком оставляет труп теста нетронутым — идеальный материал для docker logs.

Что именно случится, если процесс теста умрёт насильственно? Хронология честная. SIGKILL не оставляет процессу шансов: ни defer, ни Cleanup — всё замерло на месте. Обрываются TCP-соединения с уборщиком: рукопожатие с ACK давно пройдено, соединения держались всю сессию. Уборщик замечает тишину и начинает отсчёт reconnection timeout — десяти секунд по умолчанию; клиент, вернувшийся в окно, спасает сессию. Не вернулся — сессия объявляется мёртвой. При этом штатная уборка одиночного контейнера соседей не трогает: Terminate закрывает лишь собственное соединение этого контейнера, и уборщик продолжает дежурить для остальных. Дальше механика зачистки: по меткам собирается всё, что носит отпечаток сессии, контейнеры останавливаются и удаляются, сети и тома стираются следом, под конец уборщик прибирает самого себя. Задержка — не баг, а предохранитель: короткий сетевой шторм или пауза уснувшего ноутбука не должны стоить целого прогона, клиент успевает переподключиться. Но и не вечность: через десяток секунд после смерти процесса docker ps опустеет. Загадочные «зависшие контейнеры после kill» — вопрос двадцати секунд терпения.

### Что покажет docker ps через минуту

```go
// Прогон интеграционных тестов убит: kill -9 <pid go test>
// Ryuk включён, RYUK_RECONNECTION_TIMEOUT не задан.
// docker ps выполнен сразу после убийства — контейнеры на месте.
// Повторный docker ps — через минуту.
```

**Что выведет программа?**

- Контейнеры так и остались: SIGKILL отменил уборку навсегда
- ✅ Контейнеры исчезли: обрыв соединения и десять секунд тишины — Ryuk вычистил ресурсы сессии
- Контейнеры перезапустились: библиотека вешает на них restart-политику
- Остался один старейший контейнер: reaper чистит ресурсы по одному в минуту

Уборщик даёт клиенту окно reconnection timeout — десять секунд по умолчанию. Клиент мёртв и не вернулся, сессия объявляется мёртвой, и зачистка по меткам сносит контейнеры, сети и тома разом, а не по одному. Restart-политик библиотека не ставит: перезапуск мёртвого тестового контейнера не нужен никому.

> **Живое место: reaper не мгновенный.** Частая паника: убили зависший прогон, docker ps показывает те же контейнеры, рука тянется чистить — а через десять секунд список пуст. Это не сбой, это reconnection timeout: уборщик даёт клиенту шанс вернуться, и торопливый ручной docker rm здесь только мешает — вы удаляете то, что уборщик уже взял в работу. Подождите его, прежде чем хвататься за prune. Зеркальный случай — зомби висят дольше: проверьте RYUK_RECONNECTION_TIMEOUT и жив ли reaper-контейнер, прежде чем подозревать библиотеку.

### Утечки после падения теста и ручная гигиена

Утечка после падения — не только контейнер. За один упавший прогон остаются контейнеры со своими лимитами памяти, занятые маппнутые порты, сети-сироты, на которые больше никто не ссылается, и тома, переживают удаление контейнера. При включённом уборщике всё это сходит за десятки секунд; при выключенном — копится до ручной зачистки, и на длинном CI-дне копится быстро: порты кончаются раньше места. Диагностика начинается с фильтра по меткам: docker ps -a --filter label=org.testcontainers показывает всё, что создала библиотека, — по меткам видно и язык, и сессию. Дальше точечная зачистка тем же фильтром: удалить контейнеры, подметать сети и тома. Тома отдельная глава: анонимные уходят вместе с контейнером, а именованные — например, pgdata-it из уроков про базы — остаются на диске после любого удаления и ждут либо RemoveVolumes в Terminate, либо volume prune по меткам. Сколько места съедено, говорит docker system df: его стоит смотреть и до, и после уборки. Вслепую пущенный docker system prune -a на общем демоне — оружие массового поражения: с ним уходят и чужие образы, поэтому фильтр по меткам — не паранойя, а вежливость. Для локального docker правило одно: чистка по меткам безопасна, чистка по вдохновению — нет.

### Ручная гигиена по меткам

```go
# что осталось от тестовых прогонов
docker ps -a --filter label=org.testcontainers

# точечная зачистка по меткам: чужое не трогаем
docker rm -f $(docker ps -aq --filter label=org.testcontainers)
docker network prune -f --filter label=org.testcontainers
docker volume prune -f --filter label=org.testcontainers

# сколько места занимает docker в целом
docker system df
```

Фильтр label=org.testcontainers выдёргивает только тестовые ресурсы: чужие коллекции демона не страдают. Порядок команд отражает зависимости — сперва контейнеры, затем сети, затем тома. docker system df — не уборка, а рентген: покажет, сколько съедено образами, контейнерами, томами и кэшем сборки, до и после зачистки.

### Соберите хронологию смерти процесса

Расставьте события от убийства процесса до самозачистки уборщика.

Правильный порядок строк:

1. `процесс go test убит: ни defer, ни t.Cleanup не выполнятся`
2. `TCP-соединения теста с reaper-контейнером обрываются`
3. `Ryuk ждёт reconnection timeout — десять секунд по умолчанию`
4. `клиент не вернулся — сессия объявляется мёртвой`
5. `по меткам org.testcontainers.sessionId собираются ресурсы сессии`
6. `контейнеры останавливаются и удаляются`
7. `сети и тома сессии стираются следом`
8. `reaper-контейнер убирает и самого себя`

### Политика уборки: окружение решает

Единой политики не существует: уборка — решение окружения, и таблица ниже собирает типовые конфигурации в одну карту. Между строк таблицы живёт и настройка сессии. По умолчанию каждый процесс тестов получает собственный случайный sessionID и собственный уборщика — для go test ./... с параллельными пакетами это целая пачка reaper'ов, каждый со своими метками. Переменная TESTCONTAINERS_SESSION_ID фиксирует идентификатор: все процессы пользуются одним уборщиком, где бы ни стартовали. Значение проверяется на допустимые символы — буквы, цифры, точки, дефисы, подчёркивания; слэш в нём уронит прогон паникой ещё до первого теста, и это правильно: лучше громкий старт, чем тихая путаница меток в середине. Оборотная сторона фиксации — общие метки: на общем демоне один sessionID означает, что kill одного воркера вынесет ресурсы всех, кто делит ту же сессию, поэтому на командном демоне у каждого своя сессия. Какая бы политика ни выпала окружению, рассуждать о разделяемых ресурсах и жизненных циклах процессов качает [продвинутый курс по Go](/courses/go-advanced/): там контексты и смерть процесса разбираются глубже, чем позволяет формат урока.

### Окружение и политика уборки

| Окружение | Политика уборки | Почему так |
| --- | --- | --- |
| Локальная разработка | Ryuk включён по умолчанию, плюс Terminate в t.Cleanup | страховка на случай kill, паники и уснувшего ноутбука |
| CI с одноразовым раннером и полными правами | Ryuk включён, без настроек | одноразовый раннер и так чист, уборщик — подстраховка |
| CI без права на privileged-контейнеры | TESTCONTAINERS_RYUK_DISABLED=true | уборку ведёт сам раннер после прогона |
| Отладка упавшего теста | TESTCONTAINERS_RYUK_DISABLED=true локально | контейнер живёт для docker logs, дальше docker rm руками |
| Параллельные go-пакеты | TESTCONTAINERS_SESSION_ID фиксированный | один reaper на все процессы вместо пачки |
| Общий Docker-демон команды | Ryuk включён, чистка только по меткам | prune вслепую снесёт чужие ресурсы |

### Проверка: что меняет RYUK_DISABLED

**Вопрос:** В CI выставлена переменная TESTCONTAINERS_RYUK_DISABLED=true. Что изменится в прогоне?

- Ryuk продолжит работу, но перестанет удалять тома
- Библиотека упадёт: уборщик обязателен и неотключаем
- ✅ Reaper-контейнер не стартует, метки reap не ставятся, автозачистки нет
- Уборка ускорится: вместо контейнера будет работать процесс на хосте

Выключение убирает уборщика целиком: спавн не происходит, разрешающая метка reap на ресурсы не ставится, зачистка остаётся на совести окружения. Библиотека при этом работает штатно — слои защиты просто сокращаются до вашего t.Cleanup. Переменная окружения сильнее properties-файла, поэтому в CI настраивают именно её.

### Соберите опции уборки

Восстановите вызов Terminate: таймаут останова и именованный том, который переживёт обычное удаление.

```go
t.Cleanup(func() {
	_ = ctr.Terminate(context.Background(),
		testcontainers.____(30*time.Second),
		testcontainers.____("pgdata-it"),
	)
})
```

Подсказка: Обе опции живут в пакете testcontainers: первая принимает time.Duration, вторая — названия именованных томов строками.

Правильные ответы в пропусках: `StopTimeout`, `RemoveVolumes`

### Типовые ошибки уборки

- глушить Ryuk «ради скорости старта» на постоянной основе — мусор копится, место и порты кончаются
- отключать уборщика в CI без собственного механизма уборки — зомби переживают прогон
- ставить свои метки с префиксом org.testcontainers — библиотека отклонит их: это служебное пространство
- ждать мгновенной уборки после kill: reaper выжидает reconnection timeout — десять секунд по умолчанию
- жить по принципу «уборщик всё приберёт»: между тестами контейнеры жгут память и порты зря
- чистить общий демон вслепую через docker system prune -a — вместе с мусором уходят чужие образы
- раздавать один TESTCONTAINERS_SESSION_ID всей команде на общем демоне: kill одного воркера вынесет чужие сессии
- забывать про именованные тома: обычное удаление сносит анонимные, именованные переживают его без RemoveVolumes

> **Про песочницу: почему код не запустится в браузере.** Честная оговорка: в песочнице сайта этот урок не исполняется — уборщику нужен живой демон Docker, а браузерная среда не поднимает контейнеров. Читать это не мешает: вся механика разобрана по шагам. Эксперименты — локальные: поднимите любой тест из [урока об установке](/courses/testcontainers/lessons/02-ustanovka-pervyj-kontejner/), найдите в docker ps контейнер, чьё имя начинается с reaper_, убейте go test посреди теста и замерьте таймер уборки; затем выставьте TESTCONTAINERS_RYUK_DISABLED=true и найдите зомби руками.

### Карточки: Ryuk и ресурсы

- **Ryuk / Reaper** — Контейнер-супервизор из образа testcontainers/ryuk с именем reaper_<sessionID>: смонтированный docker.sock, TCP-рукопожатие с ACK и зачистка контейнеров, сетей и томов по меткам сессии после смерти клиента.
- **org.testcontainers.reap** — Метка-разрешение на зачистку: ставится на все ресурсы библиотеки, пока уборщик включён. Без неё уборщик ресурс не тронет, поэтому при выключенном Ryuk автозачистки нет вовсе.
- **TESTCONTAINERS_RYUK_DISABLED** — Выключатель уборщика: true — reaper не стартует и автозачистки нет. Env-переменная сильнее properties-файла. Оправдана в одноразовом CI со своей уборкой и при отладке упавшего теста.
- **RYUK_RECONNECTION_TIMEOUT** — Сколько уборщик ждёт пропавшего клиента перед зачисткой: десять секунд по умолчанию. Сосед — RYUK_CONNECTION_TIMEOUT, минута по умолчанию, на само подключение к reaper-контейнеру.
- **TESTCONTAINERS_SESSION_ID** — Фиксирует идентификатор сессии: параллельные go-пакеты получают один общий reaper вместо пачки. Значение должно быть из букв, цифр, точек, дефисов и подчёркиваний — иначе паника на старте.
- **RemoveVolumes** — Опция Terminate: список именованных томов, которые снести при уборке. Анонимные тома удаляет force-remove, а именованные переживают его — их называют явно или подметают prune'ом по меткам.

## Коротко: что запомнить

- Уборка двухслойная: t.Cleanup с Terminate — быстрая штатная зачистка, Ryuk — страховка на случай смерти процесса; первый слой можно опускать лишь осознанно.
- Reaper — sidecar testcontainers/ryuk с именем reaper_<sessionID>: смонтированный docker.sock, TCP-рукопожатие с ACK, переиспользование в рамках сессии.
- Под метки попадают контейнеры, сети и тома: метка org.testcontainers.reap ставится только при включённом уборщике, именованные тома удаляются опцией RemoveVolumes.
- TESTCONTAINERS_RYUK_DISABLED=true выключает уборщика — env сильнее properties-файла; ручная гигиена — чистка по меткам, а не prune вслепую.

## Домашнее задание

Проведите ревизию: docker ps -a --filter label=org.testcontainers и docker system df до и после зачистки по меткам. Прогоните любой тест из курса с TESTCONTAINERS_RYUK_DISABLED=true, найдите зомби через docker ps, уберите руками. Финал: верните уборщика, убейте go test посреди теста и замерьте секундомером, через сколько секунд docker ps опустеет.

## Вопросы и ответы по уроку

### Ryuk же всё приберёт — можно ли не писать t.Cleanup?

Технически мусор исчезнет, но позже и дороже. Уборщик срабатывает по смерти процесса, а не по окончании теста: до конца сессии контейнеры жгут память и держат порты, параллельные тесты наступают друг другу на ноги, задержки растут. На длинном прогоне одна брошенная база может съесть то, чего не хватит пяти живым тестам. t.Cleanup освобождает ресурс сразу и детерминированно. Слои работают в связке: дисциплина даёт скорость, страховка — гарантию, и отменять любую из них дорого.

### На CI reaper не запускается — тесты падают. Что делать?

Сначала выясните, что именно запретил демон: чаще всего окружение не даёт уборщику нужных привилегий. Если политика позволяет — включите TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true. Если раннер одноразовый и сносит окружение сам, честнее выключить уборщика TESTCONTAINERS_RYUK_DISABLED=true и положиться на механизм раннера: держать контейнер-уборщик в среде, которая умрёт через минуту, бессмысленно.

### Чем reaper лучше docker prune по расписанию в cron?

Точностью и моментом. Prune не различает, чей мусор: фильтры по возрасту или имени рано или поздно сотрут нужное чужое, а отладить такой инцидент почти невозможно — улик не останется. Уборщик зачищает строго по меткам сессии — только то, что создал этот прогон, включая сети и тома, — и делает это сразу после смерти клиента, а не в полночь по расписанию. Cron на ноутбуке разработчика и вовсе не живёт: ноутбук выключают.
