Содержание урока · 10 разделов
- Модель доступа: хост снаружи, сеть внутри
- network.New: сеть со случайным именем
- Кто с кем говорит: модель доступа — инструмент
- Подключение: Networks и NetworkAliases
- Сервис и база: адрес по алиасу
- Уборка: Remove, CleanupNetwork и изоляция
- Типовые ошибки сетей в тестах
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Первые уроки жили на маппнутом порту: тест брал Host и MappedPort и стучался к контейнеру снаружи. Как только в стенде появляется второй участник — база под сервисом, брокер под двумя сервисами, — дублировать порты наружу становится расточительно: каждая публикация — лишний мост через границу сети, потенциальное пересечение с соседним прогоном и лишняя строка в декларации. Docker решает задачу на своём уровне: пользовательские сети соединяют контейнеры напрямую, а встроенный DNS резолвит имена-алиасы во внутренние адреса. Testcontainers даёт этому типизированный путь: сеть создаёт network.New, подключают контейнер поля Networks и NetworkAliases или опция network.WithNetwork, а уборка ложится на Remove. В этом уроке — обе модели доступа, каркас сети, два рабочих примера и таблица, которая подскажет инструмент под каждый маршрут трафика. Важно, что модели не конкурируют: MappedPort остаётся главным инструментом урока про контейнер-одиночку, сети лишь добавляют второй маршрут — между контейнерами.
Модель доступа: хост снаружи, сеть внутри#
У теста и у контейнеров разные позиции в сети, и инструменты у них разные. Тест — процесс на хосте: он видит контейнер только через публикацию портов, поэтому его маршрут — ExposedPorts в декларации, затем Host и MappedPort после старта. Контейнеры между собой ходят иначе: внутри пользовательской сети Docker резолвит алиасы во внутренние адреса, и трафик идёт напрямую, минуя хост. Сценарий «testcontainers network» и построен на этом разделении: то, что раньше выглядело как «публикуй все порты подряд», распадается на две честные модели — наружу публикуется то, к чему прикасается тест, внутрь сети уходит всё остальное. Выигрыш мгновенный: база без публикации недостижима с хоста и от соседних прогонов, но прекрасно видна сервису по алиасу; у сервиса при этом один опубликованный порт вместо россыпи. Заодно исчезает целый класс флейков: прогон, публикующий всё подряд, борется за порты хоста, а сетевые адреса Docker раздаёт без ставок и конфликтов.
Каркас: сеть и два контейнера
GOfunc TestContainersTalkByAlias(t *testing.T) {
ctx := context.Background()
net, err := network.New(ctx)
if err != nil {
t.Fatalf("создание сети: %v", err)
}
t.Cleanup(func() { _ = net.Remove(ctx) })
web, err := testcontainers.Run(ctx, "nginx:alpine",
testcontainers.WithExposedPorts("80/tcp"),
network.WithNetwork([]string{"web"}, net),
)
testcontainers.CleanupContainer(t, web)
if err != nil {
t.Fatalf("запуск nginx: %v", err)
}
probe, err := testcontainers.Run(ctx, "curlimages/curl:latest",
testcontainers.WithEntrypoint("sleep", "infinity"),
network.WithNetwork([]string{"probe"}, net),
)
testcontainers.CleanupContainer(t, probe)
if err != nil {
t.Fatalf("запуск пробы: %v", err)
}
// curl http://web/ резолвит алиас web внутри сети
code, _, err := probe.Exec(ctx, []string{"curl", "-sf", "http://web/"})
if err != nil {
t.Fatalf("exec curl: %v", err)
}
if code != 0 {
t.Fatalf("запрос по алиасу вернул %d", code)
}
}network.New создаёт сеть со случайным именем, и Remove регистрируется в t.Cleanup сразу за созданием. Оба контейнера подключены опцией network.WithNetwork: nginx — под алиасом web, пробный контейнер curl — под probe. Пробе публикация портов не нужна: она ничего не отдаёт наружу, а только делает запросы. Exec выполняет curl http://web/ внутри сети — имя web резолвит встроенный DNS Docker в адрес nginx.
network.New: сеть со случайным именем#
Сеть делает одна функция — network.New из пакета network: принимает контекст и вариативные опции, возвращает *DockerNetwork и ошибку. По умолчанию создаётся мост с драйвером bridge и случайным именем: библиотека генерирует его сама, вручную имя задать нельзя — читайте поле Name из ответа. Рядом лежат поля ID и Driver: идентификатор нужен Docker-командам и уборке, драйвер — диагностике, когда тест ведёт себя по-разному на разных хостах. Метки уборщика проставляются автоматически: сеть несёт те же служебные метки сессии, что и контейнеры, поэтому супервизор зачистит её даже при смерти процесса теста. Опции меняют поведение точечно: WithDriver задаёт драйвер, WithInternal делает сеть полностью внутренней, WithLabels добавляет свои метки поверх служебных, WithIPAM настраивает адресацию, WithEnableIPv6 включает шестую версию протокола. В исходниках рядом живёт и старый путь — метод провайдера CreateNetwork с декларацией NetworkRequest, помеченный как deprecated в пользу network.New: новый вызов короче, а имена сетей придумывать не нужно.
Кто с кем говорит: модель доступа — инструмент#
| Модель доступа | Инструмент | Что объявить |
|---|---|---|
| тест или хост → контейнер | Host и MappedPort | ExposedPorts у контейнера |
| контейнер → контейнер в своей сети | алиас как имя хоста | Networks и NetworkAliases, порты не нужны |
| контейнер → несколько сетей | список Networks или WithNetwork на каждую | свой алиас каждой сети |
| контейнер → хост | HostAccessPorts и host.testcontainers.internal | порты хоста в HostAccessPorts |
| сеть без выхода наружу | network.WithInternal | Internal: true у сети |
| адрес контейнера для отладки | ContainerIP или ContainerIPs | ничего, читается после старта |
Подключение: Networks и NetworkAliases#
Контейнер подключается к сети парой полей декларации: Networks — список имён сетей, NetworkAliases — карта «сеть → список алиасов». Оба поля доступны и в ContainerRequest внутри GenericContainer, и через опции: network.WithNetwork(aliases, net) дописывает сеть и выставляет алиасы одним вызовом, WithNewNetwork создаёт сеть на месте и сразу подключает контейнер, WithBridgeNetwork присоединяет дефолтный мост без алиасов. Нюанс, который ловит почти каждый: алиасы живут только в пользовательских сетях. Попросите алиас в дефолтной bridge — опция вернёт ошибку ещё до обращения к Docker: сетевые алиасы поддерживаются только там, где сеть создана явно. Контейнер может стоять в нескольких сетях сразу: первая подключается при создании, остальные Docker прикрепляет по одной, каждая со своим списком алиасов; сколько сетей — столько и адресов, их отдают ContainerIP и ContainerIPs. Опции читаются линейно, как список аргументов docker run, а карта NetworkAliases требует помнить имя сети — на длинных декларациях опция обычно нагляднее. Поля и опции ведут к одному результату — выбирайте по стилю стенда из урока про GenericContainer.
Рефлекс из docker run: запустил базу — опубликуй 5432. В сетевом стенде это лишняя работа: сервис ходит в базу по алиасу внутри сети, и публикация порта в этом трафике не участвует вовсе. Проверяйте себя по декларации: ExposedPorts у базы появляется только тогда, когда с базой работает сам тест с хоста — сеет данные или сверяет состояние. Каждый лишний опубликованный порт — потенциальное пересечение с соседним прогоном и лишняя точка входа для чужого трафика. Меньше мостов наружу — стабильнее параллельная сборка. Таблица выше — шпаргалка: найдите свой маршрут трафика, и станет ясно, чей порт где публиковать.
Сервис и база: адрес по алиасу#
Классический стенд: сервис и его база в одном тесте. Базе — алиас db и ни одного опубликованного порта: с хоста она недостижима, и это фича, а не недосмотр. Сервис получает адрес базы через окружение: DB_HOST со значением db — тем самым алиасом, который Docker резолвит внутри сети. Приложению не важно, что на другом конце контейнер: конфигурация выглядит как в развёрнутой среде, где сервис тоже ходит в базу по имени. Тест при этом общается только с сервисом: один ExposedPorts, один MappedPort, запрос к /health. Готовность у стенда двухуровневая: база дожидается своей стратегии до возврата GenericContainer, сервис — своей, и лишь затем тест стучится в /health. Если тесту нужен прямой доступ к базе — сеять данные или сверять состояние SQL-запросами, пригодится и курс про pgx, — опубликуйте порт базе осознанно: каждый опубликованный порт — потенциальное пересечение в параллельном прогоне. Фиксированные адреса и общая сеть на весь пакет не нужны: сеть у каждого теста своя, имена внутри — свои.
Сервис и база в GenericContainer
GOfunc TestServiceAndDatabase(t *testing.T) {
ctx := context.Background()
net, err := network.New(ctx)
if err != nil {
t.Fatalf("создание сети: %v", err)
}
t.Cleanup(func() { _ = net.Remove(ctx) })
db, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
Env: map[string]string{"POSTGRES_PASSWORD": "secret"},
Networks: []string{net.Name},
NetworkAliases: map[string][]string{
net.Name: {"db"}, // сервис найдёт базу по имени db
},
// ExposedPorts нет: снаружи базу не публикуем
},
Started: true,
})
testcontainers.CleanupContainer(t, db)
if err != nil {
t.Fatalf("база: %v", err)
}
app, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "myapp/service:it",
Env: map[string]string{
"DB_HOST": "db", // алиас базы внутри сети
"DB_PORT": "5432",
},
Networks: []string{net.Name},
NetworkAliases: map[string][]string{
net.Name: {"app"},
},
ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForListeningPort("8080/tcp"),
},
Started: true,
})
testcontainers.CleanupContainer(t, app)
if err != nil {
t.Fatalf("сервис: %v", err)
}
// тест снаружи: Host и MappedPort нужны только сервису
host, err := app.Host(ctx)
if err != nil {
t.Fatalf("хост сервиса: %v", err)
}
port, err := app.MappedPort(ctx, "8080/tcp")
if err != nil {
t.Fatalf("порт сервиса: %v", err)
}
resp, err := http.Get("http://" + host + ":" + port.Port() + "/health")
if err != nil {
t.Fatalf("запрос к сервису: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("health: %d", resp.StatusCode)
}
}Здесь поля декларации вместо опций: Networks перечисляет сети, NetworkAliases привязывает алиасы к конкретной сети по её имени. База объявлена без ExposedPorts — снаружи её нет; сервис носит алиас app и ждёт 8080/tcp через wait-стратегию. Тесту доступны только Host и MappedPort сервиса: port.Port() отдаёт строку для адреса. Стенд воспроизводит боевую топологию: база невидима, у сервиса одна дверь.
Путь «docker network go» укладывается в пять шагов. Первый: network.New — сеть создана, имя случайно, метки уборщика на месте. Второй: контейнеры подключаются через Networks и NetworkAliases или опцию network.WithNetwork — по ходу декларации, рядом с Env и ExposedPorts. Третий: внутри сети сервисы находят друг друга по алиасам, а готовность каждого проверяет своя wait-стратегия — их выбор разбирает урок про ожидания. Четвёртый: тест забирает Host и MappedPort того контейнера, к которому должен прикоснуться снаружи, и делает свои запросы. Пятый: Remove в t.Cleanup снимает сеть немедленно. Шаги не зависят от образов: те же пять ходов работают и для брокеров, и для кэшей — меняются лишь имена алиасов. В общей картине курса testcontainers сети стоят рядом с модулями баз и брокеров — тем же декларативным языком, без ручных docker-команд.
Соберите сетевой стенд
Расставьте шаги стенда: от создания сети до страховки уборщика.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Проверка: алиас в дефолтной сети
Тест подключает контейнер к дефолтной сети bridge и задаёт алиас через network.WithNetworkName. Что произойдёт?
Проверка: порты базы
Сервисный контейнер ходит в базу по алиасу db внутри общей сети. Что нужно базе из публикации портов?
Что будет с параллельными сетями
Прочитайте код и выберите, что он выведет:
// Тест A и тест B помечены t.Parallel().
// В каждом:
// net, _ := network.New(ctx)
// ... контейнер postgres подключён к своей сети
// с алиасом «db» через network.WithNetworkСоберите каркас сети
Восстановите три вызова: создание сети, уборка и подключение контейнера с алиасом.
Отдельные случаи стоят одного абзаца. Фиксированная подсеть через WithIPAM полезна, когда тест проверяет адресную логику, но в параллельном прогоне две одинаковые подсети столкнутся — создание сети упадёт; держите адресацию на откуп Docker, если тест не про адреса. Контейнер → хост — отдельная модель: декларация HostAccessPorts публикует порты хоста внутрь, и из контейнера они видны на имени host.testcontainers.internal — пригодится, когда сервис должен достучаться до заглушки, поднятой самим тестом на хосте. Полностью внутренняя сеть WithInternal отсекает и выход в интернет: участникам доступен только трафик внутри сети. Для отладки многоконтейнерных стендов ContainerIPs честно покажет по адресу на каждую сеть. И помните: алиас не глобален — это имя внутри конкретной сети, в другой сети тем же именем зовут другой контейнер. Отсюда привычка: алиасы короткие и говорящие — db, cache, broker, — а уникальность им нужна только внутри своей сети, где они и живут.
Честная оговорка: в песочнице сайта сетевой стенд не поднимется — network.New и контейнеры требуют живого демона Docker, а браузерная среда исполняет код без демонов. Рабочая схема локальная: положите примеры в файл *_test.go рядом с go.mod и запустите go test — сеть, алиасы и проба по имени отработают на вашей машине. Код разобран построчно, так что чтение без Docker тоже приносит пользу. Домашний эксперимент: docker network ls до и после теста — сеть обязана исчезнуть.
Уборка: Remove, CleanupNetwork и изоляция#
Сеть — такой же ресурс прогона, как контейнер, и уборка у неё двухслойная. Первый слой ваш: net.Remove(ctx) в t.Cleanup или готовая обёртка testcontainers.CleanupNetwork(t, net), которая делает ту же регистрацию за вас; внутри Remove сигналит уборщику и просит Docker удалить сеть по идентификатору. Второй слой страховочный: метки сессии, проставленные network.New, позволяют супервизору зачистить сеть, если процесс теста умер. Убирать сразу — не педантизм: сети расходуют адресное пространство Docker, и скопление неубранных сетей к концу пакета бьёт по следующим тестам. На CI это заметнее всего: длинный пакет, где сети заводятся на каждый тест без уборки, упирается в лимиты адресации раньше, чем в лимиты памяти. Взамен вы получаете главную награду урока — изоляцию: своя сеть на тест означает, что два параллельных прогона с одинаковыми алиасами db и web не видят друг друга в упор, и никакой тест не достучится до чужой базы. Изоляция через сети дополняет изоляцию через случайные порты — вместе они делают параллельность скучной в хорошем смысле.
Типовые ошибки сетей в тестах#
- алиас на дефолтной bridge: не работает — заводите свою сеть через network.New
- поиск алиаса из теста с хоста: алиас резолвится внутри сети, снаружи нужны Host и MappedPort
- ExposedPorts базе, к которой ходит только сервис: публикация портов нужна лишь хосту
- фиксированная подсеть через WithIPAM в параллельном прогоне: пересечение адресов роняет создание сети
- забытая net.Remove: сеть висит до уборки, а адресное пространство Docker не резиновое
- один алиас на все сети: алиасы привязаны к сети, у каждой сети свой список
- контейнер в нескольких сетях с одним сетевым именем: сверяйтесь с ContainerIPs
- общая сеть на весь пакет: сети связывают тесты, своя сеть на тест дешевле и предсказуемее
Карточки: сети в тестах
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Своя сеть через network.New и алиасы закрывают общение контейнеров: сервис находит базу по имени db без публикации портов.
- Доступ снаружи — Host и MappedPort к ExposedPorts; доступ изнутри — Networks и NetworkAliases; модели разные и не взаимозаменяемы.
- Алиасы живут только в пользовательских сетях: на дефолтной bridge опции подключения вернут ошибку.
- Уборка — net.Remove в t.Cleanup или CleanupNetwork: своя сеть на тест даёт изоляцию параллельных прогонов.
Домашнее задание
Соберите стенд: сеть через network.New, nginx с алиасом web и контейнер curlimages/curl с Exec-пробой http://web/. Затем поднимите сервис и базу через GenericContainer с Networks и NetworkAliases и убедитесь, что у базы нет ExposedPorts, а сервис отвечает на /health через Host и MappedPort. Финал: подключите один контейнер к двум сетям и сравните вывод ContainerIPs.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему для связи контейнеров не нужен MappedPort?
MappedPort читает маппинг порта на хост — таблицу, которую Docker ведёт для доступа извне. Трафик между контейнерами одной сети в эту таблицу не попадает: пакеты идут по внутренним адресам сети, а имя-алиас резолвит встроенный DNS Docker. Публикация портов — мост между хостом и контейнером; когда оба конца трафика внутри сети, мост не нужен, достаточно Networks и NetworkAliases. Отсюда и экономия: десять контейнеров в сети могут иметь ноль опубликованных портов, если тест трогает лишь один.
Алиас или ContainerIP: что использовать в тесте?
Алиас. IP живёт, пока жив контейнер, меняется от прогона к прогону и известен только после старта — зашивать его в конфиг сервиса нельзя. Алиас вы придумываете до старта, подставляете в Env, и встроенный DNS сети разрулит адреса сам; адреса меняются даже внутри одного прогона после перезапуска контейнера — ещё один аргумент в пользу имён. ContainerIP и ContainerIPs оставьте для отладки: проверить, что контейнер получил адрес в каждой из своих сетей.
Что случится с сетью, если тест упал и не вызвал Remove?
Слой за слоем: t.Cleanup исполнится и при провале, и при панике — Remove сработает. Если процесс теста убит, вступает уборщик: сеть помечена служебными метками, которые network.New проставляет автоматически, и в конце сессии он зачистит и её, и контейнеры. Уборка не потеряется, но до конца сессии сеть будет зря держать адреса — поэтому Remove в Cleanup пишут всегда.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Go с нуля: базовый синтаксис и конструкции
Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.
- Писать, компилировать и запускать программы на Go
- Свободно работать с переменными, типами, операторами
- Управлять потоком выполнения: if, switch, for, range
- Хранить данные в массивах, срезах и картах