Go to the main content

Building Docker in Octo Browser

General information

While running, Octo Browser downloads the components necessary for it to function, as well as the extensions installed in your account. Therefore, if these files are not kept, they will be downloaded again each time the container starts, resulting in increased traffic usage and delays.

You can keep these files persistent in two ways.

Method 1: components folder on the host device

This method is suitable if your project architecture includes a static host device for building and running containers. Mount the components folder using the docker run command:

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

Or use volumes in docker-compose.yaml:

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

Method 2: snapshot inside the image

This method is suitable if there is no static host device: the image can be distributed to any number of devices, and containers will start with the components already in place. Authentication is performed by passing an API token as a variable. The rest of this guide covers this method.

IMPORTANT!

You may encounter operational limitations if you run more than 5 containers simultaneously and:

  • All containers share the same IP address;
  • You log into the same Octo account.

What you'll need

  • Docker, Bash, and jq on the build device (Git Bash or WSL2 on Windows).
  • An Octo Browser API token.
  • About 6 GB of free disk space: octo:base ~2.3 GB, octo:ready ~4.9 GB (shared layers with the base image are not duplicated), snapshot ~700 MB.
  • 15–40 minutes for the first full build, depending on network speed. Most of the time is spent installing packages in the base image and downloading ~1.8–2.2 GB of components.

Place all five files below in a single folder and run the build from that folder.

Step 1: creds.json

Create a file named creds.json, paste the text {"octo_api_token":"your_token_here"}, and replace "your_token_here" with your API token.

Step 2: dockerfile — base image

Create a file named 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"]

Build the base image:

docker build -t octo:base -f dockerfile .

Step 3: entrypoint.sh — launching the browser in the container

The entrypoint.sh file starts a virtual display and launches Octo Browser in headless mode every time the container starts:

Create a file named 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"

Step 4: dockerfile.final — image with the snapshot

Create a file named 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 own 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"]

You don't need to build it manually, as the script in the next step will do this for you.

Step 5: build-golden.sh — creating the snapshot

Create a file named 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

Run the script:

bash build-golden.sh

The script sequentially starts a container from octo:base, authenticates using the token, waits for the components to download, primes them by launching a one-time profile, packages ~/.Octo Browser into logged-in-state.tar.gz, builds octo:ready from it, and verifies that a fresh container starts already authenticated.

A successful completion looks like this:

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

Step 6: running the finished image

docker run -d --init --name octo-1 --security-opt seccomp:unconfined \
-e "OCTO_API_TOKEN=your_token_here" octo:ready
  • -e OCTO_API_TOKEN is required for authentication.
  • --security-opt seccomp:unconfined is required for the Chrome sandbox.

Updating the snapshot

The snapshot also locks in the component versions, so it will become outdated over time. To rebuild it, delete logged-in-state.tar.gz and run bash build-golden.sh again. If you also need to update the browser itself, rebuild the base image without the cache:

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