Skip to content

Docker Run (single container)

Use this when you already have Actual Budget running elsewhere (different host, different stack, managed instance, etc.) and only want the importer container.

Pull and run

docker pull sergienko4/israeli-bank-importer
docker run --rm --cap-add SYS_ADMIN \
  -v $(pwd)/config.json:/app/config.json:ro \
  -v $(pwd)/data:/app/data \
  -v $(pwd)/cache:/app/cache \
  -v $(pwd)/chrome-data:/app/chrome-data \
  -v $(pwd)/logs:/app/logs \
  -e TZ=Asia/Jerusalem \
  -e SCHEDULE="0 */8 * * *" \
  sergienko4/israeli-bank-importer
docker run --rm --cap-add SYS_ADMIN `
  -v "${PWD}\config.json:/app/config.json:ro" `
  -v "${PWD}\data:/app/data" `
  -v "${PWD}\cache:/app/cache" `
  -v "${PWD}\chrome-data:/app/chrome-data" `
  -v "${PWD}\logs:/app/logs" `
  -e TZ=Asia/Jerusalem `
  -e SCHEDULE="0 */8 * * *" `
  sergienko4/israeli-bank-importer

The container entrypoint is node dist/Index.js.

Production (long-running)

Drop --rm, add --restart unless-stopped, cap the memory, and run detached:

docker run -d \
  --name israeli-bank-importer \
  --restart unless-stopped \
  --cap-add SYS_ADMIN \
  --memory 2g \
  --memory-swap 2g \
  -v $(pwd)/config.json:/app/config.json:ro \
  -v $(pwd)/data:/app/data \
  -v $(pwd)/cache:/app/cache \
  -v $(pwd)/chrome-data:/app/chrome-data \
  -v $(pwd)/logs:/app/logs \
  -e TZ=Asia/Jerusalem \
  -e SCHEDULE="0 */8 * * *" \
  sergienko4/israeli-bank-importer

--memory is not optional for a long-running deployment. Without a cgroup ceiling a stalled scrape can consume every byte of host RAM before the kernel reclaims it — see Hardened defaults for the sizing.

Hardened defaults

docker run -d \
  --name israeli-bank-importer \
  --restart unless-stopped \
  --cap-drop ALL \
  --cap-add SYS_ADMIN \
  --security-opt no-new-privileges:true \
  --tmpfs /dev/shm:size=256m \
  --memory 2g \
  --memory-swap 2g \
  -v $(pwd)/config.json:/app/config.json:ro \
  -v $(pwd)/data:/app/data \
  -v $(pwd)/cache:/app/cache \
  -v $(pwd)/chrome-data:/app/chrome-data \
  -v $(pwd)/logs:/app/logs \
  -e TZ=Asia/Jerusalem \
  -e SCHEDULE="0 */8 * * *" \
  sergienko4/israeli-bank-importer
Flag Why
--cap-drop ALL + --cap-add SYS_ADMIN Drop every Linux capability except the one Camoufox needs for sandboxing
--security-opt no-new-privileges:true Block setuid escalation
--tmpfs /dev/shm:size=256m Firefox-family browsers stream to /dev/shm; allocate it explicitly
--memory 2g + --memory-swap 2g Cap the container so a stalled scrape can never exhaust host RAM. Sized from measurements: ~200 MB Node + ~1 GB worst-case bank browser + ~300 MB OCR + headroom. --max-old-space-size is not a substitute — it caps only the JS heap, and most of the footprint is native browser memory

Memory sizing

Banks are scraped one child process at a time, so the ceiling is set by the single heaviest bank — not by how many banks you configure. Thirteen banks need the same 2 GB as one.

A measured end-to-end run of the heaviest bank inside a --memory 2g container peaks at ~1.2 GB and settles back to ~400 MB between banks.

Keep PRETTY_LOGS=false. The scraper library's pino-pretty transport starts a thread-stream worker thread owning a 4 MB SharedArrayBuffer and a process exit listener. On scraper 8.6.2 the root logger was cached only when a log file was configured, so a fresh worker leaked on every log call — one bank reached 14.3 GB RSS before the kernel OOM-killed it. Scraper 8.6.3 caches the root logger per destination and fixes that at source. Since scraper 8.7.1, the transport is attached only when PRETTY_LOGS=true; NODE_ENV no longer controls it.

PRETTY_LOGS=false silences the scraper library's own log output completely unless a file destination is configured upstream. LOG_LEVEL alone does not attach a transport. The importer's logging is independent and still honours LOG_LEVEL, so -e LOG_LEVEL=trace continues to produce full importer traces.

To read the scraper's own narration for a single diagnostic run, add -e PRETTY_LOGS=true. Keep it out of long-running deployments: it attaches the pino-pretty transport described above — on scraper 8.6.3+ that is one ~4 MB worker per scrape process rather than the old leak, but it also emits ANSI colour codes into the container log. See Logging for the full procedure.

Browser fingerprint OS (CAMOUFOX_OS)

Camoufox presents the fingerprint of one operating system and renders pages with that system's fonts. The image ships only the Windows font set, so leave CAMOUFOX_OS unset or set it to windows. The scraper reads the value case-insensitively and treats anything other than linux, macos or windows as windows.

linux or macos stops the container at startup, before any bank login:

Camoufox font check failed: CAMOUFOX_OS=macos selects the macos fingerprint, but /home/node/.cache/camoufox/fonts has no macos fonts. Unset CAMOUFOX_OS or set it to one of: windows.

The same check stops startup if the Camoufox install has lost its fonts. Camoufox itself would launch with fallback fonts, and its font fingerprint would then contradict the OS it claims to be.

Volumes reference

Mount Purpose Required
/app/config.json Bank credentials + Actual Budget connection Yes (mount :ro)
/app/data Actual Budget local sync data and saved bank login tokens; must be writable Yes
/app/cache Scraper run cache Recommended
/app/chrome-data Legacy browser session (no-op with Camoufox v7.9.0+) Optional
/app/logs Rotating log files Required for /logs Telegram command

See also