Содержание урока · 10 разделов
- Флаги запуска: sandbox, headless, GPU и /dev/shm
- chrome в докере: один образ или два контейнера
- compose: браузер как сервис
- chromedp docker: память, лимиты и OOM-killer
- Диагностика: от симптома к флагу или пакету
- Симптом в контейнере → флаг или пакет
- Чек-лист контейнера с Chrome
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервер — не ноутбук, и разница проявляется в трёх местах сразу. Первое: нет дисплея. Chrome с окном не откроется — X-сервера в контейнере нет, и запуск с графическим интерфейсом упрётся в жалобу на отсутствующий X server; выход — headless-режим, в котором рендеринг идёт в память, а не на экран. Второе: нет шрифтов. Базовый образ весит десятки мегабайт именно потому, что в нём нет ничего лишнего — включая глифы; латиница отрисуется запасным начертанием, а кириллица превратится в одинаковые прямоугольники на любом скриншоте. Третье: нет sandbox. Механизм изоляции Chrome опирается на user namespaces ядра; в контейнерах они часто выключены, а процесс нередко идёт от root, — и браузер отказывается стартовать с жалобой на песочницу. Каждый сюрприз лечится отдельно: интерфейс — флагом, шрифты — пакетами, песочницу — флагом или сборкой с namespaces. Хорошая новость: половина лечения уже встроена в chromedp, и ниже видно, куда именно.
Минимальный Dockerfile с Chrome и шрифтами
GOFROM 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; когда через полгода повторится странный инцидент, вы сравните окружения за минуту, а не по памяти.
Сервис отчётов переехал в контейнер — и все снимки страниц и PDF с русскими названиями товаров вышли рядами пустых прямоугольников: претензии от клиентов за вечер. Виноват не chromedp и не код: в образе не было ни одного шрифта с кириллицей, и Chrome честно отрисовал отсутствующие глифы как «tofu». Проверка занимает минуту: сделайте скриншот страницы с русским текстом сразу после сборки образа; латиница при этом выглядит нормально и маскирует проблему до продакшена. Лечение — fonts-dejavu и fonts-noto-cyrillic в Dockerfile. Спорное мнение с условием: полный набор Noto со всеми письменностями ставить можно, но образ вырастет на сотни мегабайт — берите только те языки, которые реально рендерите.
chrome в докере: один образ или два контейнера#
Схему запуска chrome в докере выбирают из двух вариантов, и оба опираются на одни и те же флаги — разница в том, кто их задаёт. Первый — всё в одном образе: Go-бинарник и Chromium рядом, chromedp запускает процесс сама, как локально. Плюсы: простой деплой и отладка, меньше движущихся частей; минус — большой образ и общий жизненный цикл: обновление браузера пересобирает и приложение. Второй — связка из двух образов: контейнер с Chrome и открытым портом отладки 9222 плюс тонкий Go-контейнер, который подключается через NewRemoteAllocator — механику удалённого подключения вы видели в пуле из девятнадцатого урока. Плюсы: браузер масштабируется и перезапускается независимо, один инстанс обслуживает несколько приложений, у него свои лимиты памяти. Минусы: общая docker-сеть, порт, health-check и чуть больше настройки. Начинайте с одного образа и переходите к связке, когда браузер станет общим ресурсом. Приятное свойство схемы: смена раскладки не меняет код — действия, ожидания и таймауты из первых уроков работают одинаково, меняется только аллокатор, то есть строка с адресом браузера.
Клиент к браузеру в соседнем контейнере
GOallocCtx, 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 вкладки падают в крэш раньше формального лимита, и обход начинает терять страницы задолго до смерти браузера. Память инстанса смотрите метриками процесса, а не глазами в дашборде: тренд подскажет, когда пора поднимать лимит, уменьшать число вкладок или выносить браузер в отдельный контейнер со своей квотой.
Песочница сайта исполняет тематическую программу на чистом Go — без браузера, без Docker и без сети, поэтому собрать образ и запустить chrome в докере в ней не выйдет; здесь это и не нужно: вся ценность урока в реальных процессах, реальных шрифтах и реальных лимитах. Практикуйте локально: установите Docker, соберите образ из примера, сделайте скриншот с русским текстом и уроните вкладку, урезав shm. Локальный запуск — единственный честный стенд для серверного окружения. Заодно он фиксирует версии: Chromium и шрифты замораживаются Dockerfile и не уедут под вами посреди недели.
Диагностика: от симптома к флагу или пакету#
Серверные ошибки читаются как маршрутная карта: текст жалобы почти всегда называет слой — процесс, окружение, сеть или память. Старт процесса с жалобой на песочницу или 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, процесс не стартует | запуск без headless | Headless уже в 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))Соберите подключение к соседнему контейнеру
Восстановите каркас: удалённый аллокатор и обе отмены.
Соберите Dockerfile по слоям
Расставьте строки Dockerfile: базовый образ, шрифты, зависимости, сборка, запуск.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Chrome в контейнере
Что дальше: серверное окружение — последняя техническая тема курса, и следующий урок смотрит на парсер с другой стороны, со стороны сайта: этика и закон скрапинга разберут 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 от аллокатора поднимет обход снова. Запишите таблицу симптомов вашей сборки — она пригодится при переезде на настоящий сервер.
Личные заметкиПусто
Вопросы и ответы по уроку
Chrome в контейнере не стартует, в логе жалоба на sandbox. Что делать?
Сначала проверьте, что запускаете именно через chromedp: библиотека прикладывает --no-sandbox к каждому запуску сама, и руками флаг обычно не нужен. Если браузер стартует из CMD контейнера, добавьте флаг туда. Жалоба означает, что песочница браузера не смогла создать user namespaces — в контейнере их часто выключают настройки ядра. Включать namespaces и тонкие профили безопасности для скрапинга избыточно: изолированный контейнер уже выполняет роль песочницы вокруг headless-браузера.
Chrome в том же образе или отдельным контейнером — что лучше?
Начинайте с одного образа: меньше движущихся частей, проще сборка, отладка и воспроизведение на другой машине. Связка нужна, когда браузер становится общим ресурсом: один инстанс на несколько приложений, независимое обновление браузера, отдельные лимиты памяти на каждый слой. Цена связки — общая docker-сеть, порт 9222, health-check браузерного контейнера и версионирование протокола отладки. Переходите к связке, когда почувствуете боль одного образа, а не заранее «на вырост».
Пакеты шрифтов стоят, а на скриншоте всё равно квадраты. Куда смотреть?
Пересобран ли образ после добавления пакетов: слой кэшируется, и старый образ без шрифтов может жить в кэше Docker. Тот ли пакет: имена зависят от дистрибутива, проверьте поиском по репозиторию своего базового образа. Обновите кэш шрифтов командой fc-cache -f и перезапустите браузер. Если пропадает только часть символов, страница просит конкретное семейство, которого нет, — добавьте близкую замену или расширьте набор шрифтов в образе.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа