Перейти к основному содержимому

Сборка Docker в Octo Browser

Общая информация

В процессе работы Octo Browser загружает компоненты, необходимые для работы приложения, и расширения, установленные в аккаунт. Поэтому если не зафиксировать эти файлы, каждый запуск контейнера будет загружать их заново — это повышенный расход трафика и задержки в работе.

Зафиксировать их можно двумя способами.

Способ 1 — папка с компонентами на хост-устройстве

Подходит, если архитектура проекта подразумевает статичное хост-устройство для сборки и запусков контейнеров. Прокидывайте папку с компонентами в команде docker run:

-v '/srv/docker_octo/cache:/home/octo/.Octo Browser/' \

Или через volumes в docker-compose.yaml:

volumes:
- /srv/docker_octo/cache:/home/octo/.Octo Browser/

Способ 2 — слепок внутри образа

Подходит, если статичного хост-устройства нет: образ можно раздать на любое количество машин, и контейнеры стартуют уже с готовыми компонентами, а авторизация осуществляется посредством передачи переменной с API-токеном. Этому способу посвящена остальная инструкция.

ВАЖНО!

Вы можете столкнуться с ограничениями в работе, если одновременно запускаете более 5 контейнеров и при этом:

  • у всех контейнеров один IP-адрес;
  • выполняется логин в один и тот же аккаунт Octo.

Что понадобится

  • Docker, Bash и jq на машине сборки (в Windows — Git Bash или WSL2).
  • API-токен Octo Browser.
  • Около 6 ГБ свободного места: octo:base — ~2,3 ГБ, octo:ready — ~4,9 ГБ (общие слои с базовым образом не дублируются), слепок — ~700 МБ.
  • 15–40 минут на первый полный прогон в зависимости от скорости сети: основное время уходит на установку пакетов в базовый образ и загрузку ~1,8–2,2 ГБ компонентов.

Все пять файлов ниже нужно положить в одну папку, из нее же запускать сборку.

Шаг 1: creds.json

Создайте файл creds.json, вставьте текст {"octo_api_token":"ваш_токен_здесь"} и подставьте свой API-токен.

Шаг 2: dockerfile — базовый образ

Создайте файл с названием dockerfile
FROM ubuntu:22.04
ARG TZ=America/Los_Angeles
ARG DEBIAN_FRONTEND=noninteractive
ENV LANG="C.UTF-8" HOME=/home/octo \
APPIMAGE_EXTRACT_AND_RUN=1 \
OCTO_DATA="/home/octo/.Octo Browser"

RUN apt-get update && apt-get install -y \
apt-transport-https ca-certificates curl gnupg unzip jq \
libgles2 libegl1 xvfb x11vnc --no-install-recommends \
&& curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update && apt-get install -y \
fontconfig fonts-ipafont-gothic fonts-kacst fonts-noto fonts-symbola \
fonts-thai-tlwg fonts-wqy-zenhei fonts-freefont-ttf \
connect-proxy dnsutils iproute2 iptables iputils-ping net-tools \
openvpn procps socat ssh sshpass sudo tcpdump telnet traceroute \
tzdata vim-nox libgl1 libglib2.0-0 zip \
&& rm -rf /var/lib/apt/lists/*

RUN curl https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/chrome.deb \
&& apt-get update && apt-get install -y /tmp/chrome.deb && rm -f /tmp/chrome.deb \
&& rm -rf /var/lib/apt/lists/*

RUN groupadd -r octo \
&& useradd -r -g octo -s /bin/bash -m -G audio,video,sudo \
-p "$(echo 1 | openssl passwd -1 -stdin)" octo \
&& mkdir -p /home/octo/browser && chown -R octo:octo /home/octo \
&& mkdir -p /etc/sudoers.d \
&& echo 'octo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/octo && chmod 0440 /etc/sudoers.d/octo

USER octo
RUN curl -o /home/octo/browser/octo-browser.tar.gz \
https://binaries.octobrowser.net/releases/installer/OctoBrowser.linux.tar.gz \
&& tar -xzf /home/octo/browser/octo-browser.tar.gz -C /home/octo/browser \
&& rm -f /home/octo/browser/octo-browser.tar.gz
CMD ["/bin/bash"]

Соберите базовый образ:

docker build -t octo:base -f dockerfile .

Шаг 3: entrypoint.sh — старт браузера в контейнере

Файл entrypoint.sh поднимает виртуальный дисплей и запускает Octo Browser в headless-режиме при каждом старте контейнера:

Создайте файл с названием entrypoint.sh
#!/usr/bin/env bash
# entrypoint.sh — boot headless OctoBrowser inside octo:ready.
set -u
export DISPLAY=:1
OCTO_DATA="${OCTO_DATA:-/home/octo/.Octo Browser}"

rm -f /tmp/.X1-lock "$OCTO_DATA/local_port"
rm -rf /tmp/.X11-unix

Xvfb :1 -ac -screen 0 1920x1080x24 >/tmp/x.log 2>&1 &
for _ in $(seq 1 10); do
sleep 1
[ -S /tmp/.X11-unix/X1 ] && break
done

# Keep OctoBrowser as our own child so signals reach it and the container
# lives exactly as long as the browser does.
OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >/tmp/octo.log 2>&1 &
OCTO_PID=$!

term() { kill -TERM "$OCTO_PID" 2>/dev/null || true; }
trap term TERM INT

wait "$OCTO_PID"

Шаг 4: dockerfile.final — образ со слепком

Создайте файл dockerfile.final
# dockerfile.final — build octo:ready from octo:base + the golden snapshot.
# Build: docker build -t octo:ready -f ./dockerfile.final .
FROM octo:base

# --chown so the unprivileged octo user can extract the snapshot and owns the result.
COPY --chown=octo:octo logged-in-state.tar.gz /tmp/logged-in-state.tar.gz

USER octo
RUN mkdir -p "$OCTO_DATA" \
&& tar -xzf /tmp/logged-in-state.tar.gz -C "$OCTO_DATA" \
&& rm -f /tmp/logged-in-state.tar.gz

COPY --chown=octo:octo entrypoint.sh /home/octo/entrypoint.sh
RUN chmod +x /home/octo/entrypoint.sh

ENTRYPOINT ["/home/octo/entrypoint.sh"]

Вручную его собирать не нужно — это сделает скрипт из следующего шага.

Шаг 5: build-golden.sh — снятие слепка

Создайте файл build-golden.sh
#!/usr/bin/env bash
# build-golden.sh — capture a golden snapshot and build octo:ready.
# Requires: octo:base already built, creds.json with an API token, docker, jq.
# Run: bash build-golden.sh
set -euo pipefail
cd "$(dirname "$0")"

step() { printf '\n\033[36m=== %s ===\033[0m\n' "$1"; }
err() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
ok() { printf '\033[32m%s\033[0m\n' "$*"; }
warn() { printf '\033[33m%s\033[0m\n' "$*"; }
filesize(){ stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || echo 0; }

TOKEN=$(jq -r '.octo_api_token // empty' creds.json 2>/dev/null || true)
if [ -z "$TOKEN" ] || printf '%s' "$TOKEN" | grep -qE '^(PASTE|<|TOKEN)'; then
err 'creds.json does not contain octo_api_token. Format: {"octo_api_token":"..."}'
exit 1
fi
printf 'Token: %s... (length %d)\n' "${TOKEN:0:8}" "${#TOKEN}"

step "Starting golden container"
docker rm -f octo-golden 2>/dev/null || true
docker run -d --name octo-golden --security-opt seccomp:unconfined \
-e "OCTO_API_TOKEN=$TOKEN" \
--entrypoint bash octo:base -lc "sleep 3600"

step "Start Xvfb + OctoBrowser (auth via OCTO_API_TOKEN)"
docker exec octo-golden bash -lc '
rm -f /tmp/.X1-lock "$HOME/.Octo Browser/local_port"; rm -rf /tmp/.X11-unix
setsid bash -c "Xvfb :1 -ac -screen 0 1920x1080x24 >/tmp/x.log 2>&1" &
for i in $(seq 1 10); do sleep 1; [ -S /tmp/.X11-unix/X1 ] && break; done
DISPLAY=:1 setsid bash -c "OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >/tmp/octo.log 2>&1" &
'

step "Waiting for local_port to appear (up to 90 sec)"
PORT=""
for i in $(seq 1 18); do
sleep 5
PORT=$(docker exec octo-golden bash -lc 'cat "$HOME/.Octo Browser/local_port" 2>/dev/null' 2>/dev/null || true)
if [ -n "$PORT" ]; then printf ' port: %s\n' "$PORT"; break; fi
printf ' ...not yet (%ds)\n' "$((i*5))"
done
if [ -z "$PORT" ]; then
err "OctoBrowser did not come up. docker exec octo-golden cat /tmp/octo.log"
exit 1
fi

step "Checking authorization (OCTO_API_TOKEN)"
AUTHED=false
for i in $(seq 1 12); do
RESP=$(docker exec octo-golden bash -lc '
P=$(cat "$HOME/.Octo Browser/local_port" 2>/dev/null)
[ -n "$P" ] && curl -s http://127.0.0.1:$P/api/username 2>/dev/null
' 2>/dev/null || true)
printf ' [attempt %d] %s\n' "$i" "$RESP"
if printf '%s' "$RESP" | grep -q '"username"'; then AUTHED=true; break; fi
if [ $((i % 4)) -eq 0 ] && [ "$i" -lt 12 ]; then
warn " restarting the application..."
docker exec octo-golden bash -lc '
pkill -f OctoBrowser.AppImage; sleep 3
rm -f "$HOME/.Octo Browser/local_port"
DISPLAY=:1 setsid bash -c "OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >>/tmp/octo.log 2>&1" &
' >/dev/null 2>&1 || true
sleep 20
fi
sleep 5
done
if [ "$AUTHED" != "true" ]; then
err "Authorization not confirmed. Check the token / docker exec octo-golden cat /tmp/octo.log"
exit 1
fi

step "Waiting for components to download (up to 20 min)"
docker exec octo-golden bash -lc '
B="$HOME/.Octo Browser/bin"
stable=0; prev=0
for i in $(seq 1 120); do
cur=$(du -sm "$B" 2>/dev/null | cut -f1); cur=${cur:-0}
missing=""
# Ui is the GUI frontend and is not downloaded in headless mode.
for c in Octium Tentacle Profile_Parser Tentacle_Ssh userdata; do
sz=$(du -sm "$B/$c" 2>/dev/null | cut -f1); sz=${sz:-0}
[ "$sz" -ge 10 ] || missing="$missing $c"
done
if [ -z "$missing" ] && [ "$cur" = "$prev" ]; then stable=$((stable+1)); else stable=0; fi
[ "$stable" -ge 6 ] && { echo "stable ${cur}MB, all components present"; break; }
prev=$cur; sleep 10
done
[ -n "$missing" ] && echo "WARNING: did not wait for:$missing"
du -sm "$B"/* 2>/dev/null | sort -rn
'

step "Starting profile (warm-up + component-cache stabilization)"
docker exec octo-golden bash -lc '
COMP="$HOME/.Octo Browser/tmp/components"
P=$(cat "$HOME/.Octo Browser/local_port")
RESP=$(curl -s -X POST http://127.0.0.1:$P/api/profiles/one_time/start \
-H "Content-Type: application/json" \
-d "{\"profile_data\":{\"fingerprint\":{\"os\":\"win\"}},\"headless\":true,\"timeout\":120,\"flags\":[\"--no-sandbox\",\"--disable-dev-shm-usage\"]}")
UUID=$(echo "$RESP" | jq -r ".uuid // empty")
echo "UUID=$UUID resp=$RESP"
stable=0; prev=-1
for i in $(seq 1 60); do
cur=$(du -sm "$COMP" 2>/dev/null | cut -f1); cur=${cur:-0}
if [ "$cur" = "$prev" ] && [ "$cur" -gt 0 ]; then stable=$((stable+1)); else stable=0; fi
echo " components: ${cur}MB (stable=$stable)"
[ "$stable" -ge 3 ] && { echo "stable ${cur}MB"; break; }
prev=$cur; sleep 5
done
[ "${cur:-0}" -eq 0 ] && echo "WARN: component cache stayed empty (non-fatal)"
[ -n "$UUID" ] && curl -s -X POST http://127.0.0.1:$P/api/profiles/stop \
-H "Content-Type: application/json" -d "{\"uuid\":\"$UUID\"}" >/dev/null \
&& echo "profile stopped" || echo "WARN: stop failed (non-fatal)"
'

step "Capturing snapshot logged-in-state.tar.gz"
docker exec octo-golden bash -lc '
sleep 2
cd "$HOME/.Octo Browser"
# localpersist.data holds the machine HID: baking it in makes every container
# look like the same machine, and a second container steals the first session.
# The --warning/--ignore-failed-read flags keep the still-running daemon from
# turning a changed file into a fatal tar error.
tar --warning=no-file-changed --warning=no-file-removed --ignore-failed-read \
--exclude=bcache --exclude=logs --exclude=local_port --exclude=localpersist.data \
--exclude=updates \
-czf /tmp/logged-in-state.tar.gz . || [ "$?" -eq 1 ]
'
docker cp octo-golden:/tmp/logged-in-state.tar.gz ./logged-in-state.tar.gz
docker rm -f octo-golden
SZ=$(filesize logged-in-state.tar.gz)
printf 'Snapshot: %d MB\n' "$((SZ / 1024 / 1024))"

step "Building octo:ready"
docker build -t octo:ready -f ./dockerfile.final .

step "Checking octo:ready"
docker rm -f octo-check 2>/dev/null || true
docker run -d --init --name octo-check --security-opt seccomp:unconfined \
-e "OCTO_API_TOKEN=$TOKEN" octo:ready
OK=false
DEAD=false
for i in $(seq 1 18); do
sleep 5
if [ "$(docker inspect -f '{{.State.Running}}' octo-check 2>/dev/null || true)" != "true" ]; then
DEAD=true; break
fi
RESP=$(docker exec octo-check bash -lc '
P=$(cat "$HOME/.Octo Browser/local_port" 2>/dev/null)
[ -n "$P" ] && curl -s http://127.0.0.1:$P/api/username 2>/dev/null
' 2>/dev/null || true)
if printf '%s' "$RESP" | grep -q '"username"'; then
ok " OK: $RESP"; OK=true; break
fi
printf ' ...%ds: %s\n' "$((i*5))" "$RESP"
done
if [ "$OK" != "true" ]; then
# Collect the logs before removing the container, or the evidence is gone.
err "Check failed."
if [ "$DEAD" = "true" ]; then
CODE=$(docker inspect -f '{{.State.ExitCode}}' octo-check 2>/dev/null || true)
err " container is not running (exit code: ${CODE:-unknown})"
fi
printf '\n[docker logs octo-check]\n'
docker logs octo-check 2>&1 | tail -n 40 || true
printf '\n[/tmp/octo.log]\n'
docker exec octo-check bash -lc 'tail -n 40 /tmp/octo.log' 2>&1 \
|| echo "(unavailable — container not running)"
printf '\n[/tmp/x.log]\n'
docker exec octo-check bash -lc 'tail -n 20 /tmp/x.log' 2>&1 \
|| echo "(unavailable — container not running)"
fi
docker rm -f octo-check >/dev/null
if [ "$OK" = "true" ]; then
ok "DONE: octo:ready built, authorization works."
else
exit 1
fi

Запустите скрипт:

bash build-golden.sh

Скрипт последовательно поднимает контейнер из octo:base, авторизуется по токену, ждет загрузки компонентов, прогревает их запуском одноразового профиля, упаковывает ~/.Octo Browser в logged-in-state.tar.gz, собирает из него octo:ready и проверяет, что свежий контейнер стартует уже авторизованным.

Успешное завершение выглядит так:

=== Checking octo:ready ===
OK: {"username":"ваш@email"}
DONE: octo:ready built, authorization works.

Шаг 6: запуск готового образа

docker run -d --init --name octo-1 --security-opt seccomp:unconfined \
-e "OCTO_API_TOKEN=ваш_токен_здесь" octo:ready
  • -e OCTO_API_TOKEN нужен для авторизации.
  • --security-opt seccomp:unconfined требуется песочнице Chrome.

Обновление слепка

Слепок фиксирует и версию компонентов, поэтому со временем он устаревает. Чтобы пересобрать, удалите logged-in-state.tar.gz и запустите bash build-golden.sh заново. Если нужно подтянуть и новую версию самого браузера, пересоберите базовый образ без кэша:

docker build --no-cache -t octo:base -f dockerfile .