Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~18 мин +50 XP

Сети: несколько контейнеров вместе

Один контейнер в тесте — разминка, настоящий интеграционный стенд — это сервис, база и порой брокер, которым нужно найти друг друга. Библиотека закрывает сценарий «testcontainers network» целиком: собственная сеть на тест, подключение контейнеров парой полей, общение по алиасам без публикации портов и уборка сети в конце. Связка «docker network go» разбирается здесь на двух рабочих примерах: nginx и пробный клиент, затем сервис и база. Главный водораздел урока — доступ снаружи и изнутри: тест с хоста берёт Host и MappedPort, а контейнеры внутри сети ходят друг к другу по именам. Своя сеть на каждый тест даёт изоляцию: параллельные прогоны с одинаковыми алиасами не пересекаются. Под конец — таблица моделей доступа, список типовых ошибок и карточки для повторения.

Содержание урока · 10 разделов

Первые уроки жили на маппнутом порту: тест брал Host и MappedPort и стучался к контейнеру снаружи. Как только в стенде появляется второй участник — база под сервисом, брокер под двумя сервисами, — дублировать порты наружу становится расточительно: каждая публикация — лишний мост через границу сети, потенциальное пересечение с соседним прогоном и лишняя строка в декларации. Docker решает задачу на своём уровне: пользовательские сети соединяют контейнеры напрямую, а встроенный DNS резолвит имена-алиасы во внутренние адреса. Testcontainers даёт этому типизированный путь: сеть создаёт network.New, подключают контейнер поля Networks и NetworkAliases или опция network.WithNetwork, а уборка ложится на Remove. В этом уроке — обе модели доступа, каркас сети, два рабочих примера и таблица, которая подскажет инструмент под каждый маршрут трафика. Важно, что модели не конкурируют: MappedPort остаётся главным инструментом урока про контейнер-одиночку, сети лишь добавляют второй маршрут — между контейнерами.

Модель доступа: хост снаружи, сеть внутри#

У теста и у контейнеров разные позиции в сети, и инструменты у них разные. Тест — процесс на хосте: он видит контейнер только через публикацию портов, поэтому его маршрут — ExposedPorts в декларации, затем Host и MappedPort после старта. Контейнеры между собой ходят иначе: внутри пользовательской сети Docker резолвит алиасы во внутренние адреса, и трафик идёт напрямую, минуя хост. Сценарий «testcontainers network» и построен на этом разделении: то, что раньше выглядело как «публикуй все порты подряд», распадается на две честные модели — наружу публикуется то, к чему прикасается тест, внутрь сети уходит всё остальное. Выигрыш мгновенный: база без публикации недостижима с хоста и от соседних прогонов, но прекрасно видна сервису по алиасу; у сервиса при этом один опубликованный порт вместо россыпи. Заодно исчезает целый класс флейков: прогон, публикующий всё подряд, борется за порты хоста, а сетевые адреса Docker раздаёт без ставок и конфликтов.

Каркас: сеть и два контейнера

GO
func 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 и MappedPortExposedPorts у контейнера
контейнер → контейнер в своей сетиалиас как имя хостаNetworks и NetworkAliases, порты не нужны
контейнер → несколько сетейсписок Networks или WithNetwork на каждуюсвой алиас каждой сети
контейнер → хостHostAccessPorts и host.testcontainers.internalпорты хоста в HostAccessPorts
сеть без выхода наружуnetwork.WithInternalInternal: true у сети
адрес контейнера для отладкиContainerIP или ContainerIPsничего, читается после старта

Подключение: Networks и NetworkAliases#

Контейнер подключается к сети парой полей декларации: Networks — список имён сетей, NetworkAliases — карта «сеть → список алиасов». Оба поля доступны и в ContainerRequest внутри GenericContainer, и через опции: network.WithNetwork(aliases, net) дописывает сеть и выставляет алиасы одним вызовом, WithNewNetwork создаёт сеть на месте и сразу подключает контейнер, WithBridgeNetwork присоединяет дефолтный мост без алиасов. Нюанс, который ловит почти каждый: алиасы живут только в пользовательских сетях. Попросите алиас в дефолтной bridge — опция вернёт ошибку ещё до обращения к Docker: сетевые алиасы поддерживаются только там, где сеть создана явно. Контейнер может стоять в нескольких сетях сразу: первая подключается при создании, остальные Docker прикрепляет по одной, каждая со своим списком алиасов; сколько сетей — столько и адресов, их отдают ContainerIP и ContainerIPs. Опции читаются линейно, как список аргументов docker run, а карта NetworkAliases требует помнить имя сети — на длинных декларациях опция обычно нагляднее. Поля и опции ведут к одному результату — выбирайте по стилю стенда из урока про GenericContainer.

Сервис и база: адрес по алиасу#

Классический стенд: сервис и его база в одном тесте. Базе — алиас db и ни одного опубликованного порта: с хоста она недостижима, и это фича, а не недосмотр. Сервис получает адрес базы через окружение: DB_HOST со значением db — тем самым алиасом, который Docker резолвит внутри сети. Приложению не важно, что на другом конце контейнер: конфигурация выглядит как в развёрнутой среде, где сервис тоже ходит в базу по имени. Тест при этом общается только с сервисом: один ExposedPorts, один MappedPort, запрос к /health. Готовность у стенда двухуровневая: база дожидается своей стратегии до возврата GenericContainer, сервис — своей, и лишь затем тест стучится в /health. Если тесту нужен прямой доступ к базе — сеять данные или сверять состояние SQL-запросами, пригодится и курс про pgx, — опубликуйте порт базе осознанно: каждый опубликованный порт — потенциальное пересечение в параллельном прогоне. Фиксированные адреса и общая сеть на весь пакет не нужны: сеть у каждого теста своя, имена внутри — свои.

Сервис и база в GenericContainer

GO
func 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

Соберите каркас сети

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

net, err := network.(ctx) if err != nil { t.Fatalf("создание сети: %v", err) } t.Cleanup(func() { _ = net.(ctx) }) web, err := testcontainers.Run(ctx, "nginx:alpine", testcontainers.WithExposedPorts("80/tcp"), network.([]string{"web"}, net), )

Отдельные случаи стоят одного абзаца. Фиксированная подсеть через WithIPAM полезна, когда тест проверяет адресную логику, но в параллельном прогоне две одинаковые подсети столкнутся — создание сети упадёт; держите адресацию на откуп Docker, если тест не про адреса. Контейнер → хост — отдельная модель: декларация HostAccessPorts публикует порты хоста внутрь, и из контейнера они видны на имени host.testcontainers.internal — пригодится, когда сервис должен достучаться до заглушки, поднятой самим тестом на хосте. Полностью внутренняя сеть WithInternal отсекает и выход в интернет: участникам доступен только трафик внутри сети. Для отладки многоконтейнерных стендов ContainerIPs честно покажет по адресу на каждую сеть. И помните: алиас не глобален — это имя внутри конкретной сети, в другой сети тем же именем зовут другой контейнер. Отсюда привычка: алиасы короткие и говорящие — db, cache, broker, — а уникальность им нужна только внутри своей сети, где они и живут.

Уборка: 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
  • общая сеть на весь пакет: сети связывают тесты, своя сеть на тест дешевле и предсказуемее

Карточки: сети в тестах

Термин

DockerNetwork

нажмите, чтобы перевернуть
Определение

Структура, которую возвращает network.New: поля ID, Driver и Name. Имя генерируется случайно и не задаётся вручную; Remove удаляет сеть — обычно из t.Cleanup или через CleanupNetwork.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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.

Личные заметкиПусто
Хранятся только в вашем браузере (Testcontainers · Сети: несколько контейнеров вместе)

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

Почему для связи контейнеров не нужен MappedPort?

MappedPort читает маппинг порта на хост — таблицу, которую Docker ведёт для доступа извне. Трафик между контейнерами одной сети в эту таблицу не попадает: пакеты идут по внутренним адресам сети, а имя-алиас резолвит встроенный DNS Docker. Публикация портов — мост между хостом и контейнером; когда оба конца трафика внутри сети, мост не нужен, достаточно Networks и NetworkAliases. Отсюда и экономия: десять контейнеров в сети могут иметь ноль опубликованных портов, если тест трогает лишь один.

Алиас или ContainerIP: что использовать в тесте?

Алиас. IP живёт, пока жив контейнер, меняется от прогона к прогону и известен только после старта — зашивать его в конфиг сервиса нельзя. Алиас вы придумываете до старта, подставляете в Env, и встроенный DNS сети разрулит адреса сам; адреса меняются даже внутри одного прогона после перезапуска контейнера — ещё один аргумент в пользу имён. ContainerIP и ContainerIPs оставьте для отладки: проверить, что контейнер получил адрес в каждой из своих сетей.

Что случится с сетью, если тест упал и не вызвал Remove?

Слой за слоем: t.Cleanup исполнится и при провале, и при панике — Remove сработает. Если процесс теста убит, вступает уборщик: сеть помечена служебными метками, которые network.New проставляет автоматически, и в конце сессии он зачистит и её, и контейнеры. Уборка не потеряется, но до конца сессии сеть будет зря держать адреса — поэтому Remove в Cleanup пишут всегда.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Base25 уроков~12 чновичок25 уроков уже доступны

Go с нуля: базовый синтаксис и конструкции

Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.

  • Писать, компилировать и запускать программы на Go
  • Свободно работать с переменными, типами, операторами
  • Управлять потоком выполнения: if, switch, for, range
  • Хранить данные в массивах, срезах и картах