Недавно ко мне обратились с задачей: команде из десяти с лишним человек нужен рабочий браузер на сервере в Европе. Постоянные профили, чтобы не логиниться каждый день заново, загрузка и скачивание файлов, вход по ссылке с любого устройства, никаких клиентов и VPN на стороне сотрудника. Windows и 1С не нужны: вся работа идет в веб-сервисах. Расскажу, как я это собрал и на какие грабли наступил.
Сначала облако
Первой идеей было AWS AppStream 2.0: сервис отдает приложение в браузер, Chrome там есть из коробки. Но у нового аккаунта квота на AppStream по умолчанию нулевая, и пока поддержка рассматривала запрос, сроки ушли. Параллельно я поднял на EC2 демо-стенд с двумя движками для браузера в контейнере: Kasm Workspaces и Selkies из образов linuxserver.io.
Там же всплыла проблема, которую я не ожидал: часть сайтов, с которыми работала команда, встречала адреса AWS капчами или блокировкой. Крупные облака держат известные диапазоны IP, и антибот-системы относятся к ним с подозрением. В итоге от AWS отказались полностью и переехали на обычный VPS у европейского хостера.
Kasm или Selkies
До переезда я сравнил оба движка на одной машине (4 vCPU, 16 ГБ) при разрешении 1280x720. Сценарий: простой, прокрутка текста, видео 720p и полноэкранная анимация, по 90 секунд на каждую фазу. Цифры на одну активную сессию:
| Kasm CPU | Kasm трафик | Selkies CPU | Selkies трафик | |
|---|---|---|---|---|
| Простой | 0.02 ядра | ~0 | 0.10 ядра | ~0 |
| Прокрутка | 0.56 ядра | 3.0 Мбит/с | 1.5 ядра | 2.4 Мбит/с |
| Видео 720p | 0.70 ядра | 2.5 Мбит/с | 1.4 ядра | 6.6 Мбит/с |
Память одинаковая, около 0.5-0.6 ГБ на сессию, ее почти целиком съедает сам Chrome. Kasm в 2-3 раза легче по процессору и на видео отдает в 3-4 раза меньше трафика. Но экономит он за счет качества: KasmVNC кодирует только изменившиеся участки экрана и при нагрузке снижает качество и частоту кадров, на динамичном контенте это заметно как подтормаживание. Selkies кодирует поток в H.264 и держит ровные 30 кадров в секунду, картинка плавная, но платить за это приходится процессором.
Решающими оказались два момента: плавность работы и лицензия. Бесплатная версия Kasm ограничена пятью одновременными сессиями и только некоммерческим использованием, а Selkies распространяется под MPL-2.0. Выбрали Selkies.
Режимы кодирования Selkies
Selkies умеет передавать экран тремя способами, их задает переменная SELKIES_ENCODER. Первое значение в списке становится режимом по умолчанию, а если значений несколько, в боковой панели появляется переключатель, и пользователь выбирает сам.
- x264enc - весь экран кодируется как видео H.264. Это самый плавный и экономный по трафику режим на видео и анимации, браузер декодирует поток аппаратно. Минус в том, что при фиксированном битрейте (CBR) на быстрой прокрутке кодеку не хватает бит, и текст на мгновение расплывается в “мыло” и квадраты.
- x264enc-striped - тот же H.264, но экран нарезан на горизонтальные полосы, каждая кодируется отдельно и параллельно на разных ядрах, а передаются только изменившиеся полосы. Задержка ниже, на почти статичном экране трафика меньше. Одним этим режимом оставлять Selkies нельзя: однажды он стоял единственным в списке, и клиент при ошибке видео не мог переключиться на другой режим и бесконечно перезагружал страницу.
- jpeg - изменившиеся участки экрана отправляются как обычные JPEG-картинки, без видеокодека. Текст всегда четкий, артефакты не копятся от кадра к кадру, процессор отдыхает, пока на экране ничего не происходит. Зато на видео и прокрутке трафик растет в разы, ведь каждый кадр - отдельная картинка.
Отдельно есть режим Turbo (SELKIES_H264_STREAMING_MODE): H.264 кодирует каждый кадр подряд, как видеопоток, а не только изменения. Движение становится ровнее, но процессора на каждую подключенную сессию уходит больше.
Начинали мы с H.264 и Turbo, но сотрудники жаловались, что при прокрутке текст плохо читается. Команда работает с текстом и таблицами, видео смотрит редко, поэтому в итоге всех один раз переключили на JPEG, а H.264 остался в переключателе для тех, кому важнее плавность.
Архитектура
Интернет :443
│
┌─────────────────────▼──────────────────────┐
│ Caddy: HTTPS (Let's Encrypt), пароль на │
│ каждый путь /1/ ... /11/ │
└───┬──────────────┬─────────────────┬───────┘
│ /1/ │ /1/files/ │ ...
▼ ▼ ▼
chrome-1 filebrowser-1 chrome-2..11
127.0.0.1:8081 127.0.0.1:8091 127.0.0.1:...
- Каждое рабочее место - отдельный контейнер
linuxserver/chromeс Selkies: Chrome в режиме киоска, без рабочего стола, терминала и sudo. - Рядом с каждым - файловый менеджер filebrowser на той же папке, куда Chrome сохраняет загрузки. Файлы можно передавать и через боковую панель Selkies, но отдельная страница с файлами пользователям удобнее.
- Все контейнеры слушают только
127.0.0.1, наружу открыты лишь 22, 80 и 443. Это важно: Docker сам пишет правила iptables и при публикации порта на0.0.0.0обходит ufw. - Мониторинг: node_exporter, cAdvisor, Prometheus и Alertmanager с алертами в Telegram, плюс внешняя проверка доступности с другого сервера на случай, если упадет весь хост.
Общие настройки вынесены в YAML-якоря, сервис рабочего места получается коротким:
docker-compose.yml
1x-selkies-common: &common
2 PUID: "1000"
3 PGID: "1000"
4 # X11 вместо Wayland: в Wayland не работал диалог выбора файла
5 PIXELFLUX_WAYLAND: "false"
6 SELKIES_FRAMERATE: "30"
7 # первый - по умолчанию, несколько значений включают переключатель в панели
8 SELKIES_ENCODER: "x264enc,x264enc-striped,jpeg"
9 SELKIES_ENABLE_RATE_CONTROL: "true"
10 SELKIES_RATE_CONTROL_MODE: "cbr"
11 SELKIES_VIDEO_BITRATE: "4"
12 SELKIES_MICROPHONE_ENABLED: "false"
13
14x-hardening: &hardening
15 NO_DECOR: "true"
16 HARDEN_OPENBOX: "true"
17 DISABLE_SUDO: "true"
18 DISABLE_TERMINALS: "true"
19 SELKIES_COMMAND_ENABLED: "false"
20
21x-svc: &svc
22 restart: "no"
23 shm_size: 512m
24 mem_limit: 2560m
25 memswap_limit: 4096m
26 logging:
27 driver: json-file
28 options: { max-size: "10m", max-file: "3" }
29
30services:
31 chrome-1:
32 <<: *svc
33 # образ закреплен по digest, чтобы pull не обновил его без спроса
34 image: lscr.io/linuxserver/chrome@sha256:...
35 ports:
36 - "127.0.0.1:8081:3000"
37 volumes:
38 - /opt/chrome/policies:/etc/opt/chrome/policies/managed:ro
39 - /opt/data/1/Desktop:/config/Desktop
40 - /opt/data/1/Desktop:/config/Downloads
41 environment:
42 <<: [*common, *hardening]
43 TITLE: "Workspace 1"
Политика Chrome в /opt/chrome/policies принудительно ставит блокировщик рекламы: меньше рекламы на страницах - меньше процессора на кодирование видео.
Аутентификация живет целиком в Caddy, внутри контейнеров ее нет. На каждое место свой путь и свой пароль, хеши лежат в файле окружения, а не в конфиге:
Caddyfile
work.example.com {
encode zstd gzip
header X-Robots-Tag "noindex, nofollow"
redir /1 /1/ 308
handle_path /1/* {
basic_auth {
user1 {env.WS1_HASH}
}
reverse_proxy 127.0.0.1:8081 {
# не рвать WebSocket-поток при reload Caddy
stream_close_delay 5m
}
}
}
Хеш генерируется командой caddy hash-password. От перебора паролей защищает fail2ban: он читает JSON-лог Caddy и банит IP после 10 неудачных попыток за 10 минут.
Грабли
Сервер умер от собственного рестарта. Изначально у контейнеров стоял restart: unless-stopped. После перезагрузки хоста Docker стартовал все десять Chrome в одну секунду, память и swap кончились за минуту, kswapd занял оба ядра, перестали отвечать dockerd и даже sshd. Спасла только перезагрузка из панели хостера, и то со второго раза, потому что после нее все повторилось. Решение: Docker больше ничего не перезапускает сам, запуском владеет systemd-юнит, который поднимает контейнеры по два с паузой и останавливается, если свободной памяти мало:
1for i in $(seq 1 "$WORKSPACE_COUNT"); do
2 is_running "$i" && continue
3 # не стартовать новый Chrome, если памяти почти не осталось
4 if [ "$(avail_mb)" -lt "$MIN_AVAIL_MB" ]; then
5 log "ABORT at workspace $i"
6 return 0
7 fi
8 docker start "chrome-$i"
9 n=$((n + 1))
10 [ $((n % BATCH_SIZE)) -eq 0 ] && sleep "$BATCH_PAUSE"
11done
Отдельный таймер раз в три минуты поднимает не больше одного упавшего контейнера. Заодно выяснилось, что docker kill с политикой unless-stopped не считается остановкой, и убитый контейнер тут же возвращался.
Одна вкладка съедает весь сервер. Без лимитов тяжелая страница в одном браузере вытесняла всех остальных. С mem_limit на контейнер OOM killer убивает только самый большой процесс внутри него, обычно одну вкладку Chrome, а сам браузер и остальные пользователи продолжают работать. Лимит поднимали несколько раз по алертам: у разных людей очень разная привычка держать открытые вкладки.
Память надо мерить, а не считать по документации. Пустое рабочее место занимает около 750 МБ: примерно 300 МБ сам Chrome и около 350 МБ разделяемой памяти под кадровый буфер X-сервера и Selkies. Система, Docker и мониторинг забирают еще около 2.2 ГБ. Отсюда простая формула:
N = (RAM - 2.2 ГБ - запас) / 0.75 ГБ
Сервер за два месяца вырос с 2 vCPU и 4 ГБ до 8 vCPU и 16 ГБ: сначала по памяти, потом по процессору, потому что H.264 кодируется программно и при активной работе 1-1.5 ядра на сессию - нормально. На текущей конфигурации 11 мест работают с запасом.
zram вместо swap на диске. Разделяемая память X-сервера вытесняется только в swap. Swap на диске VPS медленный, а в zram эти страницы сжимаются примерно 23:1, так что сжатый swap в памяти почти бесплатен. Еще одна находка: если не задать MAX_RES, X-сервер выделяет кадровый буфер под 16K, и эти нули занимают весь лимит swap контейнера.
Терялись русские буквы. При быстром наборе на кириллице пропадали или менялись местами символы, английский текст печатался нормально. Причина: Selkies вводит русские символы через xdotool, а при раскладке только us тот на каждое нажатие временно переназначает свободную клавишу, и при быстром наборе не успевает. Помогла фиксированная раскладка us,ru в X-сервере каждого контейнера, без переключения по Alt+Shift, иначе сочетание пользователя переключало бы раскладку и на сервере.
Обновление Chrome сломало выбор файлов. Начиная с версии 145 в собственном диалоге Chrome кнопкой по умолчанию стала “Отмена”, и двойной клик по файлу закрывал диалог, ничего не загрузив. Лечится установкой xdg-desktop-portal-gtk в образ: тогда Chrome использует системный диалог, где двойной клик и Enter работают как положено.
Окно, которое нельзя вернуть. В киоске нет панели задач, и пользователь, свернувший Chrome, оставался перед черным экраном. Кнопки окна убрала настройка Chrome “использовать системную рамку”, а небольшой скрипт раз в секунду разворачивает окно обратно, если его свернули двойным кликом по панели вкладок.
Обновления только руками. Образы закреплены по digest, а не по тегу latest. Новая версия Chrome проверяется на одном рабочем месте, потом раскатывается партиями по 2-3 контейнера. Случайный docker compose pull больше ничего не сломает.
О минусах
HTTP Basic Auth - компромисс: в браузере нет кнопки “выйти”, пароль кешируется до закрытия браузера, второго фактора нет. Готовый портал входа на плагине Caddy я пробовал, но он навязывал TOTP и ломал редирект после входа, и я откатился. Для более широкой аудитории стоит поставить перед рабочими местами Forward Auth с Authelia или сделать собственную страницу входа под задачи команды.
Рабочее место - это общий экран, а не персональная сессия: если два человека зайдут под одним паролем, они будут управлять одним браузером. Поэтому у каждого сотрудника свое место и свой пароль.
И это администрирование, а не готовый сервис: мониторинг памяти, алерты, обновления образов и резервное копирование профилей остаются на вас. Для управления местами я написал небольшую утилиту, которая включает, отключает и сбрасывает место и меняет пароль, и инструкцию для администратора клиента, чтобы рутина не требовала моего участия.
Итог
Браузер в контейнере на обычном VPS оказался отличной заменой VDI для команды, которая работает в вебе: каждое место обходится в сотни рублей, а не в тысячи, вход по ссылке, никаких лицензий Windows. Но Selkies - это конструктор, а не продукт: память, запуск, раскладка, диалоги и обновления пришлось доводить руками, и без мониторинга такая система долго не проживет. Если вам нужен похожий сервер для команды, напишите мне: я настраиваю такое в рамках администрирования серверов и мониторинга.
Нужна помощь с инфраструктурой?
Проведу аудит серверов, деплоя и мониторинга, предложу план и возьму поддержку на себя.