Files
Anthias/docker-compose.yml.tmpl
Viktor Petersson b57c1a16a7 feat(viewer,server): 1 GB SBC enablement — low-RAM degradation gates (#2915)
Boards with < 1.5 GiB MemTotal (Pi 2/Pi 3 1GB, Pi 4 1GB, Rock Pi 4 1GB,
generic-arm64 1GB SKUs) OOM-cycled when QtMultimedia loaded a 4K HEVC asset
alongside the two QtWebEngine renderers introduced by #2905. On-device repro
on a 1 GB Rock Pi 4 confirmed `global_oom` on the container's bash process
followed by a restart loop, plus the kernel keeping sshd in banner-exchange
until power-cycle. This patch puts the device into a graceful degraded mode
before the OOM cascade fires.

  * bin/upgrade_containers.sh — exports ANTHIAS_LOW_RAM=1 when
    TOTAL_MEMORY_KB < 1572864 (1.5 GiB), 0 otherwise. Threshold cleanly
    splits the 1 GB SKUs from the 2 GB+ SKUs in the supported fleet.
  * docker-compose.yml.tmpl — forwards ANTHIAS_LOW_RAM to the viewer
    container.
  * anthias_host_agent — publishes host:total_mem_kb to Redis alongside the
    existing host:board_subtype so server-side gates can read MemTotal
    without re-opening /proc/meminfo themselves.
  * anthias_common.board — adds LOW_RAM_THRESHOLD_KB, get_total_mem_kb(),
    is_low_ram_device() helpers.
  * anthias_webview (view.cpp) — when ANTHIAS_LOW_RAM=1, aliases webView2
    onto webView1 so the rest of the dual-buffer logic still runs but never
    spawns a second Chromium renderer (~100 MB physical RAM saved per
    device). UX: page swap is in-place with a brief blank during load, no
    preloaded crossfade.
  * anthias_server.processing — extends the codec gate with a low-RAM 1080p
    resolution cap. Above-1080p uploads on low-RAM boards are rejected at
    upload with the existing recipe machinery, extended to inject
    `-vf scale=1920:1080:force_original_aspect_ratio=decrease` so the
    operator's re-encode lands within the envelope. If an upload fails BOTH
    codec and resolution gates, the codec message wins but the recipe folds
    in the downscale so a single re-encode satisfies both.
  * Diagnostics page — Memory card surfaces a "Low-RAM mode" badge with the
    threshold MiB so operators can see why the device degraded. /api/v2/info's
    `memory` field gains a `low_ram: bool` for API clients.
  * docs/board-enablement.md — rewrote the stale `--hwdec=drm-copy` /
    per-codec dispatch text (removed in #2905); documented the known rkvdec
    mainline limitation on Armbian 6.18 (HEVC stateless engages via
    `-hwaccel drm` but produces decode errors; H.264 has no v4l2_request
    binding in +rpt1 7.1.3) and the new low-RAM mode.

Tests cover the four matrix cells (low/high-RAM × in/over-cap), the recipe
shape with and without `cap_to_1080p`, the cap defence against
unknown / zero dimensions, host_agent's MemTotal parser, and the API
endpoint's new low_ram field.

Out of scope (separate work): HEVC HW-decode on arm64 — depends on an
upstream rkvdec driver fix landing in Debian-shipped Armbian kernels;
Anthias does not maintain its own kernel/distro.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:48:46 +02:00

166 lines
5.9 KiB
Cheetah

# vim: ft=yaml.docker-compose
services:
anthias-server:
image: ghcr.io/screenly/anthias-server:${DOCKER_TAG}-${DEVICE_TYPE}
build:
context: .
dockerfile: docker/Dockerfile.server
ports:
- 80:8080
environment:
- MAC_ADDRESS=${MAC_ADDRESS}
- HOST_USER=${USER}
- HOME=/data
- LISTEN=0.0.0.0
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
- redis
devices:
- "/dev/vchiq:/dev/vchiq"
restart: always
volumes:
- resin-data:/data
- /home/${USER}/.anthias:/data/.anthias
- /home/${USER}/anthias_assets:/data/anthias_assets
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
io.balena.features.supervisor-api: '1'
logging:
driver: journald
options:
tag: anthias-server
anthias-viewer:
image: ghcr.io/screenly/anthias-viewer:${DOCKER_TAG}-${DEVICE_TYPE}
build:
context: .
dockerfile: docker/Dockerfile.viewer
mem_limit: ${VIEWER_MEMORY_LIMIT_KB}k
depends_on:
- anthias-server
environment:
- HOME=/data
- PORT=8080
- NOREFRESH=1
- LISTEN=anthias-server
# Forward the host's system locale into the viewer container so
# AnthiasWebview can send a matching Accept-Language header
# (issue 480 — multi-language URL assets used to render in
# English regardless of the Pi's locale). upgrade_containers.sh
# sources /etc/default/locale before running envsubst, so these
# substitute to whatever `update-locale` wrote. When the host has
# no locale configured these substitute to empty strings; the
# viewer's start_viewer.sh treats an empty value as "unset" so
# downstream consumers see the container image's defaults rather
# than an empty LANG.
- LANG=${LANG}
- LANGUAGE=${LANGUAGE}
- LC_ALL=${LC_ALL}
# ``1`` on boards with < 1.5 GiB MemTotal. AnthiasViewer reads
# this to fall into single-QWebEngineView mode (no preloaded
# crossfade — the inactive renderer cost ~100 MB physical RAM
# per box, untenable on 1 GB SBCs). Derived from /proc/meminfo
# in bin/upgrade_containers.sh so the server side
# (host_agent → ``host:total_mem_kb`` in Redis) and the viewer
# side both observe the same total.
- ANTHIAS_LOW_RAM=${ANTHIAS_LOW_RAM}
privileged: true
restart: always
# cage (the wlroots kiosk compositor used on Pi 5 / x86 /
# arm64 viewers) doesn't always release the DRM master on
# SIGTERM — its event loop blocks on a libinput shutdown
# path that hangs on certain kernels, and the kernel's GPU
# driver leaves dangling references that prevent the next
# container start from acquiring DRM master. ``stop_grace_period``
# caps how long ``docker compose down/restart`` waits before
# SIGKILLing the container; 3s is short enough that an
# upgrade rolls in a reasonable time even if cage hangs.
# ``stop_signal: SIGKILL`` skips the SIGTERM-then-wait dance
# entirely on intentional stops — same effect as a kernel
# OOM killer landing on the viewer, which the next ``up``
# cleans up from automatically.
stop_grace_period: 3s
stop_signal: SIGKILL
shm_size: ${SHM_SIZE_KB}kb
volumes:
- resin-data:/data
- /home/${USER}/.asoundrc:/data/.asoundrc
- /home/${USER}/.anthias:/data/.anthias
- /home/${USER}/anthias_assets:/data/anthias_assets
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
io.balena.features.supervisor-api: '1'
logging:
driver: journald
options:
tag: anthias-viewer
anthias-celery:
# Runs on the same image as anthias-server with a CMD override.
# Shipping one image instead of two is the point — server and celery
# share their entire root filesystem (base apt + venv + app source),
# and a separate celery image was duplicating ~825 MB extracted of
# identical content per device. See refactor: drop celery image.
image: ghcr.io/screenly/anthias-server:${DOCKER_TAG}-${DEVICE_TYPE}
# ``mem_limit`` keeps a runaway celery task (e.g. a HEIC →
# WebP convert on a decompression-bomb fixture) from eating
# the box's RAM and starving the viewer. 60% of host (computed
# in bin/upgrade_containers.sh) leaves comfortable headroom on
# every supported SBC.
mem_limit: ${CELERY_MEMORY_LIMIT_KB}k
# ``nice -n 19 ionice -c 3`` keeps any subprocess celery
# spawns (ffprobe, Pillow's libheif binding) at idle priority
# so the on-device viewer always wins CPU + IO contention. No
# current task is CPU-bound enough that this matters in
# practice, but the wrappers are cheap insurance against a
# pathological input.
command: >
nice -n 19 ionice -c 3
celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias
--loglevel=info --scheduler celery.beat.Scheduler
depends_on:
- anthias-server
- redis
environment:
- HOME=/data
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
devices:
- "/dev/vchiq:/dev/vchiq"
restart: always
volumes:
- resin-data:/data
- /home/${USER}/.anthias:/data/.anthias
- /home/${USER}/anthias_assets:/data/anthias_assets
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
io.balena.features.supervisor-api: '1'
logging:
driver: journald
options:
tag: anthias-celery
redis:
image: ghcr.io/screenly/anthias-redis:${DOCKER_TAG}-${DEVICE_TYPE}
build:
context: .
dockerfile: docker/Dockerfile.redis
ports:
- 127.0.0.1:6379:6379
restart: always
volumes:
- redis-data:/var/lib/redis
logging:
driver: journald
options:
tag: anthias-redis
volumes:
resin-data:
redis-data: