Repository navigation
Expand file tree
/
Copy pathdocker-compose.balena.dev.yml.tmpl
More file actions
252 lines (246 loc) · 11.2 KB
/
Copy pathdocker-compose.balena.dev.yml.tmpl
File metadata and controls
252 lines (246 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# 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.<pid>` 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
build:
context: .
dockerfile: ./docker/Dockerfile.server
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
build:
context: .
dockerfile: ./docker/Dockerfile.viewer
depends_on:
- anthias-server
# Issue #480 — see docker-compose.balena.yml.tmpl. balena Device or
# Service Variables (e.g. LANG=nl_NL.UTF-8) are injected by the
# supervisor at runtime; no compose entry needed for AnthiasWebview
# to pick them up.
environment:
- HOME=/data
- PORT=8080
- NOREFRESH=1
- LISTEN=anthias-server
# See docker-compose.balena.yml.tmpl: lets start_viewer.sh quit the
# Plymouth boot splash over the host system bus (mounted by
# io.balena.features.dbus below) so it releases DRM master before
# cage starts. 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
build:
context: .
dockerfile: ./docker/Dockerfile.redis
ports:
- 127.0.0.1:6379:6379
restart: always
volumes:
- redis-data:/var/lib/redis
volumes:
resin-data:
redis-data: