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

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

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

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

Docker и серверное окружение

На ноутбуке всё работало, а в контейнере Chrome не стартует, скриншоты с русским текстом выходят рядами квадратов, вкладки падают пачками — три классических сюрприза, на которых ломается первый же chromedp docker-запуск. Поиск «chrome в докере» приводит не к одной проблеме, а к трём: нет графического интерфейса, нет шрифтов, нет пользовательских пространств имён для песочницы браузера. В этом уроке собираем минимальный образ с Chromium и кириллическими шрифтами, разбираем флаги запуска и их дефолты в chromedp, соединяем Go-контейнер с браузерным контейнером по ws://chrome:9222, считаем память против лимитов и сводим диагностику в таблицу: симптом — флаг или пакет.

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

Сервер — не ноутбук, и разница проявляется в трёх местах сразу. Первое: нет дисплея. Chrome с окном не откроется — X-сервера в контейнере нет, и запуск с графическим интерфейсом упрётся в жалобу на отсутствующий X server; выход — headless-режим, в котором рендеринг идёт в память, а не на экран. Второе: нет шрифтов. Базовый образ весит десятки мегабайт именно потому, что в нём нет ничего лишнего — включая глифы; латиница отрисуется запасным начертанием, а кириллица превратится в одинаковые прямоугольники на любом скриншоте. Третье: нет sandbox. Механизм изоляции Chrome опирается на user namespaces ядра; в контейнерах они часто выключены, а процесс нередко идёт от root, — и браузер отказывается стартовать с жалобой на песочницу. Каждый сюрприз лечится отдельно: интерфейс — флагом, шрифты — пакетами, песочницу — флагом или сборкой с namespaces. Хорошая новость: половина лечения уже встроена в chromedp, и ниже видно, куда именно.

Минимальный Dockerfile с Chrome и шрифтами

GO
FROM golang:1.23-bookworm

RUN apt-get update && apt-get install -y \
        chromium \
        fonts-liberation \
        fonts-dejavu \
        fonts-noto-cyrillic \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .
RUN go build -o parser .
CMD ["./parser"]

Один образ и с Go, и с Chrome — самый короткий путь для первого запуска: базовый golang-bookworm, поверх него chromium и три пакета шрифтов — liberation для латиницы, dejavu и noto для кириллицы. Имена пакетов зависят от дистрибутива: в Alpine префикс font- и другой набор — проверяйте поиском по репозиторию своего базового образа. В бою образ делят на build- и runtime-слой, но для учебного стенда этого достаточно.

Флаги запуска: sandbox, headless, GPU и /dev/shm#

Запуск с флагами — это ExecAllocator, знакомый по второму уроку, и здесь самое важное место урока: читайте дефолты, прежде чем копировать чужие. Из исходников библиотеки видно, что DefaultExecAllocator уже включает Headless и disable-dev-shm-usage, а при каждом запуске процесса chromedp прикладывает --no-sandbox — то есть защита от трёх сюрпризов из коробки есть, и вручную перечислять их не нужно. Что остаётся вам. Первое: --disable-gpu — на сервере без видеокарты рендеринг через GPU даёт артефакты и лишние падения; добавьте chromedp.Flag("disable-gpu", true) в свой набор опций, это единственный из серверных флагов, которого в дефолтах нет. Второе: шрифты флагами не лечатся — только пакетами в образе. Третье: /dev/shm. Docker по умолчанию монтирует этому каталогу 64 МБ; Chrome складывает туда разделяемую память рендереров, и на тяжёлых страницах вкладки начинают падать с крэшами — флаг disable-dev-shm-usage переносит временные файлы на /tmp, а альтернатива — поднять shm_size в compose. Если браузер запускаете вручную через docker run, эти флаги прописываете сами в CMD. И привычка из практики: держите набор флагов в одном месте — рядом с аллокатором — и логируйте его при старте вместе с версией Chromium; когда через полгода повторится странный инцидент, вы сравните окружения за минуту, а не по памяти.

chrome в докере: один образ или два контейнера#

Схему запуска chrome в докере выбирают из двух вариантов, и оба опираются на одни и те же флаги — разница в том, кто их задаёт. Первый — всё в одном образе: Go-бинарник и Chromium рядом, chromedp запускает процесс сама, как локально. Плюсы: простой деплой и отладка, меньше движущихся частей; минус — большой образ и общий жизненный цикл: обновление браузера пересобирает и приложение. Второй — связка из двух образов: контейнер с Chrome и открытым портом отладки 9222 плюс тонкий Go-контейнер, который подключается через NewRemoteAllocator — механику удалённого подключения вы видели в пуле из девятнадцатого урока. Плюсы: браузер масштабируется и перезапускается независимо, один инстанс обслуживает несколько приложений, у него свои лимиты памяти. Минусы: общая docker-сеть, порт, health-check и чуть больше настройки. Начинайте с одного образа и переходите к связке, когда браузер станет общим ресурсом. Приятное свойство схемы: смена раскладки не меняет код — действия, ожидания и таймауты из первых уроков работают одинаково, меняется только аллокатор, то есть строка с адресом браузера.

Клиент к браузеру в соседнем контейнере

GO
allocCtx, allocCancel := chromedp.NewRemoteAllocator(
        context.Background(), "ws://chrome:9222",
)
defer allocCancel()

taskCtx, taskCancel := chromedp.NewContext(allocCtx)
defer taskCancel()

err := chromedp.Run(taskCtx,
        chromedp.Navigate("https://example.com"),
        chromedp.WaitVisible("body"),
)

chrome — имя сервиса из compose-файла: Docker DNS разрешает его внутри общей сети. NewRemoteAllocator принимает адрес вида ws://хост:порт и сам уточняет полный путь браузера через /json/version — в исходниках это функция modifyURL. Дальше всё как с локальным аллокатором: NewContext заводит вкладку, Run исполняет действия, отмена закрывает соединение. Если соединение не установилось, проверяйте сеть и порт раньше кода.

compose: браузер как сервис#

compose-файл — самый удобный способ собрать связку: браузерный сервис с Chromium и Go-приложение рядом в одной сети. Для браузера задайте четыре вещи. Команду запуска с флагами — headless, no-sandbox, remote-debugging-port=9222 и disable-dev-shm-usage, если браузер стартует не из chromedp. Порты наружу не публикуйте без нужды: клиентам внутри сети compose хватает имени сервиса, а открытый вовне порт отладки — это консоль управления браузером, доступная любому. shm_size в мегабайтах, если оставляете разделяемую память вместо флага. restart: unless-stopped, чтобы упавший Chrome поднимался сам, и лимит памяти, посчитанный по вкладкам из предыдущего раздела. Приложению передайте один адрес — ws://chrome:9222 — через переменные окружения, и код из примера выше останется без изменений. Health-check по /json/version — хорошая привычка: он покажет живой порт отладки раньше, чем клиент словит connection refused, и оркестратор не отправит трафик в мёртвый контейнер.

chromedp docker: память, лимиты и OOM-killer#

Связка chromedp docker обязана считаться с лимитами контейнера: cgroup-ограничение на память режет не только ваше приложение, но и весь Chrome вместе с вкладками. Арифметика из пула здесь жёстче, чем кажется: вкладка лёгкой страницы — десятки мегабайт, тяжёлого SPA — сотни; четыре вкладки SPA уже держат гигабайт. Когда лимит контейнера выбран, ядро убивает самый прожорливый процесс — им будет Chrome, а не ваш парсер, и все горутины разом получат context canceled из пятнадцатого урока. Отсюда правило планирования: лимит контейнера равен памяти Go-приложения плюс число вкладок, умноженное на их тяжесть, плюс запас на пики. Запас обязателен: под давлением cgroup вкладки падают в крэш раньше формального лимита, и обход начинает терять страницы задолго до смерти браузера. Память инстанса смотрите метриками процесса, а не глазами в дашборде: тренд подскажет, когда пора поднимать лимит, уменьшать число вкладок или выносить браузер в отдельный контейнер со своей квотой.

Диагностика: от симптома к флагу или пакету#

Серверные ошибки читаются как маршрутная карта: текст жалобы почти всегда называет слой — процесс, окружение, сеть или память. Старт процесса с жалобой на песочницу или X server — слой флагов и пакетов. Вкладки падают пачками на тяжёлых страницах — слой /dev/shm или памяти контейнера. Клиент не достучался до ws://chrome:9222 — слой docker-сети и портов: сервисы в разных сетях, порт не опубликован, опечатка в имени. Квадраты на рендере — слой шрифтов, и никакой код это не починит. Логи контейнера читайте целиком: жалобы Chrome на память и песочницу пишутся в stderr и попадают в docker logs, а не в ваш Go-лог — связать их по времени с ошибками Run значит собрать половину диагноза. Общий приём тот же, что в отладке из двадцать второго урока: воспроизведите минимальным кодом — Navigate на простую страницу с русским текстом и скриншот, — и симптом локализуется за минуты, до чтения конфигов. Таблица ниже собирает типовые пары «симптом — лечение»; держите её под рукой при первой сборке образа: она экономит вечер блужданий по логам.

Симптом в контейнере → флаг или пакет#

СимптомПричинаЛечение
квадраты на кириллице в скриншотах и PDFв образе нет шрифтовfonts-dejavu, fonts-noto-cyrillic в Dockerfile
Running as root without --no-sandboxпесочнице нужны user namespaces, их нет--no-sandbox — chromedp добавляет сам
вкладки падают пачками на тяжёлых страницах/dev/shm всего 64 МБ--disable-dev-shm-usage или shm_size: 1g
жалоба на X server, процесс не стартуетзапуск без headlessHeadless уже в DefaultExecAllocator
артефакты рендера, странные сбои графикиGPU на сервере отсутствует--disable-gpu через chromedp.Flag
connection refused на ws://chrome:9222сеть и порт между контейнерамиобщая docker-сеть, порт 9222 открыт

Чек-лист контейнера с Chrome#

  • chromium и шрифты кириллицы — в образ до первой сборки
  • первый скриншот с русским текстом — сразу после сборки
  • --no-sandbox, --headless, --disable-dev-shm-usage уже в дефолтах chromedp
  • --disable-gpu добавьте сами: на сервере видеокарты нет
  • для связки: контейнер Chrome с портом 9222 в общей сети
  • лимит памяти = приложение + вкладки × тяжесть + запас
  • прогрев первым Run без короткого дедлайна — как на локальной машине
  • крэши вкладок ищите в /dev/shm и лимите памяти раньше, чем в коде

Проверка: --no-sandbox руками

Нужно ли добавлять --no-sandbox в опции ExecAllocator, чтобы запустить Chrome в контейнере от root?

Проверка: крэши вкладок

Браузер в контейнере запущен вручную, без chromedp: на тяжёлых страницах вкладки падают с крэшами. Какой флаг лечит в первую очередь?

Что сделает лимит памяти

Прочитайте код и выберите, что он выведет:

// docker run --memory=512m parser
// обход: 6 вкладок тяжёлого SPA одновременно
err := chromedp.Run(taskCtx, chromedp.Navigate(url))

Соберите подключение к соседнему контейнеру

Восстановите каркас: удалённый аллокатор и обе отмены.

allocCtx, allocCancel := chromedp.(context.Background(), "ws://chrome:9222") allocCancel() taskCtx, taskCancel := chromedp.NewContext(allocCtx) taskCancel()

Соберите Dockerfile по слоям

Расставьте строки Dockerfile: базовый образ, шрифты, зависимости, сборка, запуск.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: Chrome в контейнере

Термин

--no-sandbox

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

Отключает песочницу браузера, которой в контейнере без user namespaces не на чем работать. chromedp добавляет флаг к каждому запуску процесса сам — из allocate.go.

1 / 6

Что дальше: серверное окружение — последняя техническая тема курса, и следующий урок смотрит на парсер с другой стороны, со стороны сайта: этика и закон скрапинга разберут robots.txt, ToS и персональные данные. Практика — только локально: песочница сайта исполняет программы без контейнеров и сети, и ни Dockerfile, ни ws://chrome:9222 в ней не воспроизвести. Домашняя проверка на честность: соберите образ из урока и сделайте скриншот страницы с русским текстом — на нём должно быть видно каждое слово, а не ряды прямоугольников; затем запустите обход на шести вкладках с лимитом контейнера 512 МБ и прочитайте, что вернёт Run после того, как OOM-killer заберёт Chrome. Эту ошибку вы теперь умеете воспроизводить нарочно — и лечить. Собранный образ — это ещё и воспроизводимость: тот же Chromium, те же шрифты и те же флаги на любой машине, от ноутбука до боевого кластера; чините окружение в Dockerfile, а не правками на живом сервере — иначе через месяц никто не скажет, чем боевой контейнер отличается от собранного сегодня.

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Серверное Chrome-окружение — три сюрприза: нет дисплея, нет шрифтов, нет sandbox.
  • Headless и disable-dev-shm-usage уже в DefaultExecAllocator, --no-sandbox chromedp добавляет сам.
  • --disable-gpu — единственный серверный флаг, который добавляете вы: видеокарты на сервере нет.
  • Квадраты на кириллице — это шрифты: fonts-dejavu и fonts-noto-cyrillic в образ до сборки.
  • Первый скриншот с русским текстом делайте сразу после сборки образа.
  • Два пути: Chrome в одном образе с Go или связка контейнеров по ws://chrome:9222.
  • NewRemoteAllocator принимает ws://хост:9222 и уточняет путь через /json/version.
  • Лимит контейнера = приложение + вкладки × тяжесть + запас; иначе OOM-killer убьёт Chrome.
  • Крэши вкладок в докере ищите в /dev/shm и памяти раньше, чем в коде.

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

Соберите образ из урока и добейтесь скриншота с читаемым русским текстом. Затем соберите связку: контейнер с Chrome на порту 9222 и Go-контейнер с NewRemoteAllocator — и прогоните через неё обход из трёх страниц. Урежьте контейнеру память до 512 МБ, откройте шесть вкладок тяжёлого SPA и поймайте момент, когда OOM-killer убивает Chrome: убедитесь, что следующий Run вернёт context canceled, а новый NewContext от аллокатора поднимет обход снова. Запишите таблицу симптомов вашей сборки — она пригодится при переезде на настоящий сервер.

Личные заметкиПусто
Хранятся только в вашем браузере (Chromedp · Docker и серверное окружение)

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

Chrome в контейнере не стартует, в логе жалоба на sandbox. Что делать?

Сначала проверьте, что запускаете именно через chromedp: библиотека прикладывает --no-sandbox к каждому запуску сама, и руками флаг обычно не нужен. Если браузер стартует из CMD контейнера, добавьте флаг туда. Жалоба означает, что песочница браузера не смогла создать user namespaces — в контейнере их часто выключают настройки ядра. Включать namespaces и тонкие профили безопасности для скрапинга избыточно: изолированный контейнер уже выполняет роль песочницы вокруг headless-браузера.

Chrome в том же образе или отдельным контейнером — что лучше?

Начинайте с одного образа: меньше движущихся частей, проще сборка, отладка и воспроизведение на другой машине. Связка нужна, когда браузер становится общим ресурсом: один инстанс на несколько приложений, независимое обновление браузера, отдельные лимиты памяти на каждый слой. Цена связки — общая docker-сеть, порт 9222, health-check браузерного контейнера и версионирование протокола отладки. Переходите к связке, когда почувствуете боль одного образа, а не заранее «на вырост».

Пакеты шрифтов стоят, а на скриншоте всё равно квадраты. Куда смотреть?

Пересобран ли образ после добавления пакетов: слой кэшируется, и старый образ без шрифтов может жить в кэше Docker. Тот ли пакет: имена зависят от дистрибутива, проверьте поиском по репозиторию своего базового образа. Обновите кэш шрифтов командой fc-cache -f и перезапустите браузер. Если пропадает только часть символов, страница просит конкретное семейство, которого нет, — добавьте близкую замену или расширьте набор шрифтов в образе.

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

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

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

Testcontainers25 уроков~8 чпрактика25 уроков уже доступны

Testcontainers в Go: интеграционные тесты с Docker

Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.

  • Поднимать реальные зависимости в Docker из кода теста
  • Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
  • Ждать готовность сервиса wait-стратегиями вместо sleep
  • Собирать стенды из нескольких контейнеров и своего образа