# vim: ft=yaml version: "2" services: anthias-server: image: ghcr.io/screenly/anthias-server:${GIT_SHORT_HASH}-${BOARD} # A crash must not cost the device its storage. The host's # core_pattern is the kernel default `core`, a relative path, so a # dump lands in the crashing process's working directory, which is # WORKDIR /usr/src/app: inside the container, on no mount. With # core_uses_pid=1 every crash adds a fresh `core.` rather than # replacing one, and nothing sweeps them. They are invisible to # `docker images`, and `docker image prune` cannot touch a live # container's writable layer, so the usual disk investigation comes # back clean while the card fills. A device in the field reached # 24.2 GB of them in anthias-celery (issue 3417). Sentry already # reports crashes, so on a device a dump buys nothing and costs the # card. Anchored here and aliased by every service below: the rule # is "any of our containers", not "the one that happened to crash". ulimits: &no-core-dumps core: 0 ports: - 80:8080 environment: - HOME=/data - LISTEN=0.0.0.0 - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/0 depends_on: - redis # No `devices:` here, and none needed. HDMI-CEC is driven by the # anthias-viewer container, which is privileged and therefore # already sees every /dev/cec* node; this service asks it over the # Redis command bus (src/anthias_server/lib/cec_client.py). # # That indirection is what makes CEC work on balena at all. The # compose file here is baked into the release from a workstation, # nothing on-device can enumerate the host's adapters, and a # statically listed node that turns out to be absent would stop the # container from starting. /dev/vchiq used to be listed for libcec, # which is gone. restart: always volumes: - resin-data:/data labels: io.balena.features.supervisor-api: '1' anthias-viewer: image: ghcr.io/screenly/anthias-viewer:${GIT_SHORT_HASH}-${BOARD} ulimits: *no-core-dumps depends_on: - anthias-server # Issue #480 — to localise URL-asset rendering, set LANG (and # optionally LANGUAGE) as a balena Device or Service Variable in the # dashboard, e.g. LANG=nl_NL.UTF-8. balena's supervisor injects # dashboard variables as env vars into the running container at # boot, so no entry is needed in this `environment:` block — # AnthiasWebview reads QLocale::system() and builds Accept-Language # from whatever LANG/LANGUAGE the supervisor passed in. environment: - HOME=/data - PORT=8080 - NOREFRESH=1 - LISTEN=anthias-server # Lets start_viewer.sh reach the host systemd over the host system # bus (mounted by io.balena.features.dbus below) to quit the # Plymouth boot splash before launching the display. balenaOS # leaves Plymouth running and on x86 it holds DRM master, which # blocks cage. See release_boot_splash() in bin/start_viewer.sh. - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket privileged: true restart: always shm_size: ${SHM_SIZE} volumes: - resin-data:/data labels: io.balena.features.supervisor-api: '1' io.balena.features.dbus: '1' anthias-celery: # Runs on the same image as anthias-server with a CMD override. # See docker-compose.yml.tmpl for context on the merge. image: ghcr.io/screenly/anthias-server:${GIT_SHORT_HASH}-${BOARD} ulimits: *no-core-dumps command: > nice -n 19 ionice -c 3 celery -A anthias_server.celery_tasks.celery worker -B -n worker@anthias --loglevel=info --schedule /tmp/celerybeat-schedule depends_on: - anthias-server - redis environment: - HOME=/data - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/0 # No `devices:` here, and none needed. HDMI-CEC is driven by the # anthias-viewer container, which is privileged and therefore # already sees every /dev/cec* node; this service asks it over the # Redis command bus (src/anthias_server/lib/cec_client.py). # # That indirection is what makes CEC work on balena at all. The # compose file here is baked into the release from a workstation, # nothing on-device can enumerate the host's adapters, and a # statically listed node that turns out to be absent would stop the # container from starting. /dev/vchiq used to be listed for libcec, # which is gone. restart: always volumes: - resin-data:/data labels: io.balena.features.supervisor-api: '1' # The WiFi setup portal. Started by the supervisor on every boot and # every release apply, which is safe because the DAEMON decides whether # a portal is needed -- there is no systemd here to hang an # ExecCondition on, and nothing in a compose file can gate a service. # # wifi-connect 2026.10.11 is the release that makes that true: before # it, a device online over ethernet alone was not considered online # (NetworkManager reports connectivity as "unknown" wherever the check # is unavailable, and only an associated radio counted), so every start # raised an access point at a perfectly healthy screen. Verified on a # wired Pi 5 and Pi 3 B+ against 2026.10.11: exits 0 in ~20 seconds # with "already online; the portal is not needed", no access point, no # state file. The same test against 2026.10.10 raises one. # # Do NOT pin this below 2026.10.11. 2026.10.12 matters too: # missing branding is a warning there rather than a refusal to # start, which is what makes staging it on the shared volume # safe, and a guessed `limited` verdict no longer raises an # access point at a wired screen on an isolated LAN. # # Measured on balenaOS 2.83.18 (NetworkManager 1.28.0) booted in QEMU, # because the three facts this service depends on there are not # obvious from the Debian path: # # * ConnectivityCheckAvailable is FALSE, exactly as on Debian and # Raspberry Pi OS -- so the "unknown" fallback above is the code # path that runs here too. (meta-balena's current bbappend does # add `concheck` to PACKAGECONFIG; this released build predates # that, and either way the daemon handles both.) `Connectivity` # itself still answers 4/FULL, which is the trap: it is optimistic # when no probe ran. # # * supervisor0 is a BRIDGE that NetworkManager reports as # `connected (externally)`. Upstream's online check counts only # ethernet, wifi and modem devices, which is what stops every # balena device reading as permanently online and never raising # a portal at all. # # * The host D-Bus socket reaches the container through the label # below: the pinned image run with exactly this env and mount # exits 3 ("no WiFi device was found" -- the VM has no radio), # not 5, which is what a failure to reach NetworkManager looks # like. anthias-wifi-connect: image: ghcr.io/screenly-labs/wifi-connect:2026.10.13 ulimits: *no-core-dumps # Serving DHCP, which does not survive Docker's NAT, and binding the # access point's own address on the wireless device. network_mode: host # `always` is the schedule here, because there is nothing else that # can be one. # # `restart: "no"` does NOT mean "start once per boot", on balena or # anywhere else. It means nothing will start the container again # once it has exited, and that is by design at both layers: # # * The supervisor starts a service only if it has never been # started. Its own words, in compose/app.ts: "Only start a # Service if we have never started it before and the service # matches target! This is so the engine can handle the restart # policy configured for the container." An exited service is # neither Installing nor Installed, so it is left alone. The one # exception is a container the Engine failed to start because a # host interface was not ready, matched by its error message. # # * balena-engine, like Docker, only starts containers at daemon # start if their policy is `always` or `unless-stopped`. # # So on Debian `restart: "no"` is right because systemd starts the # container; here there is no systemd, and nothing else volunteers. # Confirmed on balenaOS 9.0.3 with supervisor 18.2.2: the # container's start and finish times were unchanged across a reboot. # # Reboot-only would not be enough anyway. The common failure is a # screen that stays powered on while the WiFi password changes # around it, and telling an operator to power-cycle a wall-mounted # display is the advice this feature exists to replace. # # What makes `always` affordable is that the daemon waits before # exiting (below). Without that it is a spin: measured on a wired # Pi 5, a container start every eighteen seconds, 4,680 a day, with # 19,000 log lines to match -- which on balena ship to the cloud. restart: always environment: # NetworkManager lives on the host; io.balena.features.dbus below # is what puts its socket in this container. - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket # A PREFIX: the portal appends four random characters, so five # screens in one room do not all broadcast one name. - PORTAL_SSID=Anthias Setup - WIFI_CONNECT_BRAND_NAME=Anthias # Staged on the shared volume by anthias-server's startup # script, because ansible does not run here. Missing files # are a warning upstream as of 2026.10.12, not a refusal to # start, so losing the race on a first boot costs the portal # its colours and not its existence. - WIFI_CONNECT_BRAND_CSS=/data/hotspot/brand/brand.css - WIFI_CONNECT_BRAND_LOGO=/data/hotspot/brand/logo.svg - WIFI_CONNECT_DEFAULT_THEME=dark # NOT 80: anthias-server publishes 80:8080 on the host. - PORTAL_LISTENING_PORT=8000 - WIFI_CONNECT_QR_OUTPUT=/data/hotspot # How anthias-server learns the SSID, passphrase and portal # address to put on the screen, and whether a portal is live. - WIFI_CONNECT_STATE_FILE=/data/hotspot/state.json - WIFI_CONNECT_NO_QR_STDOUT=true # Idle, not a session cap, as of 2026.10.9. - ACTIVITY_TIMEOUT=900 # The cadence `restart: always` turns into a schedule, as of # 2026.10.13. Five minutes between checks on a screen that is # online -- the check is one D-Bus round trip -- and six hours # after a setup page nobody used, because that one just held the # display for a quarter of an hour. A site with nothing to join # therefore spends a minute in twenty-four showing setup instead # of content, which is the same budget the systemd timer and its # backoff give the Debian path. - WIFI_CONNECT_RECHECK_INTERVAL=300 - WIFI_CONNECT_RETRY_AFTER=21600 volumes: - resin-data:/data labels: io.balena.features.dbus: '1' redis: image: ghcr.io/screenly/anthias-redis:${GIT_SHORT_HASH}-${BOARD} ulimits: *no-core-dumps ports: - 127.0.0.1:6379:6379 restart: always volumes: - redis-data:/var/lib/redis volumes: resin-data: redis-data: