---
title: "Docker и серверное окружение"
description: "chromedp docker: образ с Chrome и шрифты кириллицы, sandbox-флаги и /dev/shm. Dockerfile, связка контейнеров по ws://chrome:9222 и лимиты памяти контейнера."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/23-docker-server/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Chromedp в Go: автоматизация Chrome без Selenium», урок 23 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/chromedp/lessons/23-docker-server/*

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

Сервер — не ноутбук, и разница проявляется в трёх местах сразу. Первое: нет дисплея. Chrome с окном не откроется — X-сервера в контейнере нет, и запуск с графическим интерфейсом упрётся в жалобу на отсутствующий X server; выход — headless-режим, в котором рендеринг идёт в память, а не на экран. Второе: нет шрифтов. Базовый образ весит десятки мегабайт именно потому, что в нём нет ничего лишнего — включая глифы; латиница отрисуется запасным начертанием, а кириллица превратится в одинаковые прямоугольники на любом [скриншоте](/courses/chromedp/lessons/07-skrisnty/). Третье: нет 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; когда через полгода повторится странный инцидент, вы сравните окружения за минуту, а не по памяти.

> **Живое место: квадраты на скриншотах.** Сервис отчётов переехал в контейнер — и все снимки страниц и PDF с русскими названиями товаров вышли рядами пустых прямоугольников: претензии от клиентов за вечер. Виноват не chromedp и не код: в образе не было ни одного шрифта с кириллицей, и Chrome честно отрисовал отсутствующие глифы как «tofu». Проверка занимает минуту: сделайте скриншот страницы с русским текстом сразу после сборки образа; латиница при этом выглядит нормально и маскирует проблему до продакшена. Лечение — fonts-dejavu и fonts-noto-cyrillic в Dockerfile. Спорное мнение с условием: полный набор Noto со всеми письменностями ставить можно, но образ вырастет на сотни мегабайт — берите только те языки, которые реально рендерите.

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

Схему запуска chrome в докере выбирают из двух вариантов, и оба опираются на одни и те же флаги — разница в том, кто их задаёт. Первый — всё в одном образе: Go-бинарник и Chromium рядом, chromedp запускает процесс сама, как локально. Плюсы: простой деплой и отладка, меньше движущихся частей; минус — большой образ и общий жизненный цикл: обновление браузера пересобирает и приложение. Второй — связка из двух образов: контейнер с Chrome и открытым портом отладки 9222 плюс тонкий Go-контейнер, который подключается через NewRemoteAllocator — механику удалённого подключения вы видели в пуле из [девятнадцатого урока](/courses/chromedp/lessons/19-parallelnyj-pul/). Плюсы: браузер масштабируется и перезапускается независимо, один инстанс обслуживает несколько приложений, у него свои лимиты памяти. Минусы: общая 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 из [пятнадцатого урока](/courses/chromedp/lessons/15-tajmauty-i-prochnost/). Отсюда правило планирования: лимит контейнера равен памяти Go-приложения плюс число вкладок, умноженное на их тяжесть, плюс запас на пики. Запас обязателен: под давлением cgroup вкладки падают в крэш раньше формального лимита, и обход начинает терять страницы задолго до смерти браузера. Память инстанса смотрите метриками процесса, а не глазами в дашборде: тренд подскажет, когда пора поднимать лимит, уменьшать число вкладок или выносить браузер в отдельный контейнер со своей квотой.

> **Про песочницу: почему Docker не показать в песочнице.** Песочница сайта исполняет тематическую программу на чистом 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
- ✅ Нет: chromedp прикладывает --no-sandbox к каждому запуску процесса сам
- Только при подключении через NewRemoteAllocator
- Да, но только в образах на базе Debian

В исходниках allocate.go видно: ExecAllocator прикладывает --no-sandbox к каждому запуску Chrome независимо от ваших опций. Понимать смысл флага всё равно нужно: песочница браузера опирается на user namespaces, которых в контейнере может не быть. Добавлять флаг руками не вредно, но и не требуется.

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

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

- ✅ --disable-dev-shm-usage: Docker даёт /dev/shm всего 64 МБ
- --disable-gpu
- --no-sandbox
- --user-data-dir

Разделяемая память рендереров живёт в /dev/shm, а Docker по умолчанию отводит ей 64 МБ — тяжёлым страницам не хватает, вкладки падают. Флаг переносит временные файлы на /tmp; альтернатива — поднять shm_size. В chromedp он уже включён в DefaultExecAllocator, поэтому крэши под chromedp — повод смотреть память и лимиты.

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

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

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

- Run зависнет навсегда: лимит памяти останавливает планировщик Go
- ✅ OOM-killer убьёт процесс Chrome, и все вкладки получат context canceled
- Chrome сам выгрузит лишние вкладки и продолжит обход
- Run вернёт ошибку сети на первой же навигации

Cgroup-лимит распространяется на все процессы контейнера, и самым прожорливым будет Chrome: ядро гасит процесс, chromedp теряет соединение и отменяет контекст — та же механика, что при смерти браузера. Ретраи в мёртвый контекст бессмысленны, лечит пересоздание от аллокатора. Считайте лимит заранее: приложение плюс вкладки плюс запас.

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

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

```go
allocCtx, allocCancel := chromedp.___(context.Background(), "ws://chrome:9222")
___ allocCancel()

taskCtx, taskCancel := chromedp.NewContext(allocCtx)
___ taskCancel()
```

Подсказка: Удалённый аллокатор — NewRemoteAllocator, обе отмены планируются defer.

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

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

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

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

1. `FROM golang:1.23-bookworm`
2. `RUN apt-get update && apt-get install -y chromium fonts-liberation fonts-dejavu fonts-noto-cyrillic`
3. `WORKDIR /app`
4. `COPY go.mod go.sum ./`
5. `RUN go mod download`
6. `COPY . .`
7. `RUN go build -o parser .`
8. `CMD ["./parser"]`

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

- **--no-sandbox** — Отключает песочницу браузера, которой в контейнере без user namespaces не на чем работать. chromedp добавляет флаг к каждому запуску процесса сам — из allocate.go.
- **--disable-dev-shm-usage** — Переносит временные файлы рендереров с /dev/shm на /tmp: в Docker /dev/shm по умолчанию 64 МБ, и тяжёлые вкладки падают. Уже включён в DefaultExecAllocator.
- **Шрифты кириллицы** — fonts-liberation, fonts-dejavu, fonts-noto-cyrillic: без них русский текст на скриншотах и в PDF — квадраты. Флагами не лечится, только пакетами в образе.
- **chromedp.NewRemoteAllocator** — Подключение к запущенному браузеру по ws://хост:9222: контейнер с Chrome плюс тонкий Go-контейнер. Полный путь уточняется через /json/version (modifyURL).
- **Память контейнера** — Cgroup-лимит режет Chrome целиком: вкладки × тяжесть + приложение + запас. Лимит выбран — OOM-killer убьёт Chrome, все вкладки получат context canceled.
- **--disable-gpu** — Единственный из серверных флагов, которого нет в дефолтах chromedp: на машине без видеокарты рендеринг через GPU даёт артефакты. Добавляйте chromedp.Flag.

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

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

- Серверное 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 и перезапустите браузер. Если пропадает только часть символов, страница просит конкретное семейство, которого нет, — добавьте близкую замену или расширьте набор шрифтов в образе.
