# BoardRipper Docker Compose Configuration # # Quick start: # 1. docker compose up -d # 2. Open http://localhost:8081 # # Volumes: # - ./data → uploaded board files (persists across restarts) # - ./library → mount your board file folders here for browsing in the Library panel # Mount individual folders as subdirectories of /library, e.g.: # - /path/to/MacBooks:/library/MacBooks:ro # - /path/to/iPhones:/library/iPhones:ro # These will appear as top-level folders in the Library browser. # # Environment variables: # PORT — internal server port (default: 8080, no need to change) # STATIC_DIR — frontend files location (default: /static, set by image) # DATA_DIR — upload storage (default: /data, mapped via volume) # LIBRARY_DIR — library root for file browser (default: /library) services: boardripper: build: . # The image ships USER 65532:65532 (the distroless nonroot convention) # so a hypothetical RCE doesn't own the bind-mounted volumes. The # default `docker compose up -d` flow creates ./data and ./library on # the host as root, which UID 65532 then can't write — `databank.Open` # log.Fatals at boot, the container exits, and the user gets a restart # loop on a clean install. The simplest fix that preserves the # documented one-command flow is to override USER → root for this # compose deploy, matching what `scripts/deploy-remote.sh` already # does on the NAS. Comment this out (or remove it) and either # (a) chown your host bind-mount targets to 65532:65532, or # (b) switch ./data to a named volume — Docker initializes named # volumes from the image's pre-chowned /data, preserving 65532. user: "0:0" ports: - "8081:8080" # Change 8081 to your preferred external port volumes: - ./data:/data # Board file upload storage - ./library:/library # Board file library (read-only browsing) # Docker socket — required for self-update. The running container # calls the Docker Engine API to pull the new image and swap itself # out. Without this mount, `Update & Restart` will be greyed out and # the drop-to-update fallback won't work either. Combined with the # `user: "0:0"` above, this is what makes in-app updates possible on # a fresh install. Remove only if you don't want self-update. - /var/run/docker.sock:/var/run/docker.sock restart: unless-stopped # No container healthcheck: the scratch image has no shell, curl, or wget, # so it cannot self-healthcheck. `test: ["NONE"]` disables any healthcheck # inherited from the image (the interval/timeout/retries/start_period keys # are no-ops once the test is NONE, so they're omitted). Liveness is polled # externally — the self-update orchestrator hits /api/health after a swap # (see the secure update pipeline in CLAUDE.md / docs/RELEASE_RUNBOOK.md). healthcheck: test: ["NONE"] # Hard memory cap. `mem_limit` maps to --memory on every Compose version; # `deploy.resources.limits.memory` was ignored by early Compose v2 outside # swarm (the container ran uncapped) and is honoured by current releases # (measured 2026-09-12 on Compose 5.5) — `mem_limit` is the one that has # always worked, so it stays. The runtime derives a soft GOMEMLIMIT at # 90% of this (see configureMemoryLimit in main.go) so the GC reclaims before # the hard cap OOM-kills, and GODEBUG=madvdontneed=1 (Dockerfile) returns # freed pages promptly. Tune down on a small NAS together with # PDFINDEX_POOL_MAX; the pdfium/wazero linear memory is the main floor. mem_limit: 2g environment: - PORT=8080 - PDFINDEX_POOL_MAX=2 # max concurrent pdfium/wazero workers; reduce to 1 on NAS with <1 GB free RAM # GITHUB_TOKEN is no longer needed — updates use offline-signed manifests # from ripperdoc.de and ghcr.io (see docs/RELEASE_RUNBOOK.md).