Ctrl K
ComunidadGuías…

docker-compose.yml.tmpl

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# vim: ft=yaml.docker-compose

services:
  anthias-server:
    image: ghcr.io/screenly/anthias-server:${DOCKER_TAG}-${DEVICE_TYPE}
    # 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:
      - 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
      # HTTP proxy support (GH #3239). upgrade_containers.sh sources
      # /etc/anthias/proxy.env before envsubst, so these substitute to
      # the configured proxy (or empty strings when none is set, which
      # every client treats as "no proxy"). NO_PROXY already includes the
      # anthias-server/redis service names so internal traffic bypasses
      # the proxy.
      - HTTP_PROXY=${HTTP_PROXY}
      - HTTPS_PROXY=${HTTPS_PROXY}
      - NO_PROXY=${NO_PROXY}
      - http_proxy=${http_proxy}
      - https_proxy=${https_proxy}
      - no_proxy=${no_proxy}
      # Chunk size for large browser uploads, for an operator behind a
      # proxy that caps request bodies. Empty when unset, which the
      # server reads as "use the default". The durable place to set it
      # is `upload_chunk_size_mb` in ~/.anthias/anthias.conf, which
      # wins over this and survives an upgrade; balena devices use a
      # dashboard variable, which the supervisor injects into every
      # service. This entry only carries a value exported into the
      # environment of the run that renders the template.
      - ANTHIAS_UPLOAD_CHUNK_SIZE_MB=${ANTHIAS_UPLOAD_CHUNK_SIZE_MB}
    # Gate startup on redis actually answering PING (see the
    # healthcheck on the redis service below) — plain service_started
    # only orders container creation, so uvicorn's first Channels /
    # Celery touch could still race a redis that hadn't finished
    # loading its RDB. NOTE: docker-compose only; the balena
    # supervisor doesn't support depends_on conditions, so fleet
    # devices rely on each consumer's own reconnect logic instead.
    depends_on:
      redis:
        condition: service_healthy
    # No `devices:` here, deliberately. This service needs no hardware
    # access at all: HDMI-CEC is driven by the anthias-viewer container
    # (privileged, so it already sees every /dev/cec*) and reached over
    # the Redis command bus — see src/anthias_server/lib/cec_client.py.
    # /dev/vchiq used to be listed for libcec's display-power probe,
    # which is gone.
    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}
    ulimits: *no-core-dumps
    build:
      context: .
      dockerfile: docker/Dockerfile.viewer
    mem_limit: ${VIEWER_MEMORY_LIMIT_KB}k
    # redis: the viewer subscribes to the anthias.viewer pub/sub
    # channel and publishes the display resolution; wait for a PING
    # so neither hits "connection refused" at boot. (compose-only —
    # see the note on anthias-server's depends_on.)
    depends_on:
      anthias-server:
        condition: service_started
      redis:
        condition: service_healthy
    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}
      # HTTP proxy support (GH #3239) — also picked up by QtWebEngine
      # (Chromium reads http_proxy/https_proxy on Linux) for web assets.
      - HTTP_PROXY=${HTTP_PROXY}
      - HTTPS_PROXY=${HTTPS_PROXY}
      - NO_PROXY=${NO_PROXY}
      - http_proxy=${http_proxy}
      - https_proxy=${https_proxy}
      - no_proxy=${no_proxy}
    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}
    ulimits: *no-core-dumps
    # ``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
    # redis is both the broker and the beat heartbeat; gate on a PING
    # so the worker doesn't burn its first connection retries during
    # compose startup. (compose-only — see anthias-server's note.)
    depends_on:
      anthias-server:
        condition: service_started
      redis:
        condition: service_healthy
    environment:
      - HOME=/data
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
      # HTTP proxy support (GH #3239) — the celery worker downloads URL
      # assets (requests, honours these) and shells out to yt-dlp (inherits
      # the container env).
      - HTTP_PROXY=${HTTP_PROXY}
      - HTTPS_PROXY=${HTTPS_PROXY}
      - NO_PROXY=${NO_PROXY}
      - http_proxy=${http_proxy}
      - https_proxy=${https_proxy}
      - no_proxy=${no_proxy}
    # See the note on anthias-server: no hardware access needed. Celery
    # runs the periodic display-power query and the schedule, but it
    # asks anthias-viewer to do the CEC work rather than opening the
    # devices itself.
    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

  # Captive-portal WiFi provisioning — Screenly-Labs/wifi-connect, a
  # ground-up replacement for the balena-os tool Anthias shipped until
  # 2026.04 (that one had no ARMv6 binary and a `set -e` bug that made
  # its captive-portal branch unreachable; see "chore: drop wifi-connect
  # service end-to-end"). Upstream publishes a multi-arch image, so
  # there is nothing for Anthias to build: the tag is pinned to an exact
  # CalVer release rather than `2026.10` or `latest` so an upstream patch
  # can never land on a fleet without a deliberate bump here.
  anthias-wifi-connect:
    image: ghcr.io/screenly-labs/wifi-connect:2026.10.13
    ulimits: *no-core-dumps
    # Pinned, because three other things address this container BY
    # NAME and compose's generated name is `anthias-anthias-wifi-
    # connect-1`: the systemd unit's `docker start`, the
    # `docker logs anthias-wifi-connect` in the docs, and the
    # pre-create in upgrade_containers.sh. Without this the unit fails
    # with "No such container" and the portal never starts — which is
    # invisible until a device is actually offline, because every
    # other code path ends at the ExecCondition skip.
    #
    # Also the name the pre-rebrand container used, so journald tags
    # and operator muscle memory carry over.
    container_name: anthias-wifi-connect
    # The documented exception to "never network_mode: host" in
    # CLAUDE.md, and the only one. That rule exists so host-only facts
    # (IPs, MACs, interfaces) are routed through host_agent -> Redis
    # instead of punching a hole in bridge isolation to read them, and
    # every Anthias service still obeys it. This service is not reading
    # a host fact: it is *serving DHCP*, an L2 broadcast protocol that
    # does not survive Docker's DNAT, and pinning that server to the
    # WiFi interface — so it cannot answer a lease request from the
    # customer's ethernet LAN — needs an interface that exists in the
    # namespace. There is no bridge-side equivalent to route through.
    #
    # The exception is bounded: nothing Anthias wrote runs in this
    # namespace. The image is upstream's, built FROM scratch, and
    # Anthias's own detection of it goes the ordinary way — through
    # the address resolver the splash page already uses.
    network_mode: host
    # The whole flow is NetworkManager calls over the system bus. Root
    # inside the container is what makes them pass NM's authorization
    # (NM short-circuits uid 0 before polkit), so no `privileged: true`
    # is needed — the default capability set already carries
    # CAP_NET_BIND_SERVICE for udp/53 and udp/67.
    environment:
      - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket
      # A PREFIX as of wifi-connect 2026.10.6: the portal appends four
      # random characters, so five screens unboxed in one room do not all
      # broadcast "Anthias Setup" for a phone to collapse into one entry
      # and join at random. The /hotspot screen shows the full name, so
      # there is nothing for an installer to work out.
      - PORTAL_SSID=Anthias Setup
      - WIFI_CONNECT_BRAND_NAME=Anthias
      # Themes the portal the user opens on their phone so it matches the
      # setup screen on the display in front of them: plum canvas, yellow
      # Connect, Anthias mark in the header. Role tokens only, with one
      # documented exception — see the header in the file.
      - WIFI_CONNECT_BRAND_CSS=/data/hotspot/brand/brand.css
      - WIFI_CONNECT_BRAND_LOGO=/data/hotspot/brand/logo.svg
      # Pinned, which also removes the portal's "Switch theme" control.
      # The plum canvas is the brand; which scheme a two-minute setup page
      # uses is ours to decide, not a question to put to someone who is
      # trying to get a screen online. Needs wifi-connect >= 2026.10.2,
      # where --default-theme is actually read.
      - WIFI_CONNECT_DEFAULT_THEME=dark
      # NOT port 80: anthias-server publishes 80:8080 on the host, so
      # binding the portal there fails outright (upstream reports it as
      # PortalUnavailable). 8000 matches the port the previous Anthias
      # integration used. The cost is that an OS connectivity probe,
      # which always goes to port 80, lands on the Anthias admin UI
      # instead of the portal; the portal is reached through DHCP option
      # 114 (RFC 8910), which iOS 14+, Android 11+, macOS and Windows 11
      # all honour, and through the address printed on the setup screen.
      - PORTAL_LISTENING_PORT=8000
      # Where the join QR lands for /hotspot to render. resin-data is
      # mounted at /data in anthias-server too, so the file the portal
      # writes here is the file the page reads.
      - WIFI_CONNECT_QR_OUTPUT=/data/hotspot
      # How the portal tells anthias-server what to put on the screen:
      # the SSID actually on the air, the per-run passphrase and the
      # portal's address, in one atomic write. Its PRESENCE is also how
      # the server knows a portal run is live -- wifi-connect writes it
      # once it commits to raising the access point and removes it when
      # the run ends, which the QR files cannot say because they are
      # written before the portal's own online check.
      #
      # Read by anthias_server.lib.wifi_connect; the path is asserted
      # against that module in tests/test_wifi_connect.py.
      - WIFI_CONNECT_STATE_FILE=/data/hotspot/state.json
      # The same QR also goes to stdout as Unicode blocks, which is a
      # genuine fallback on a plain console but is unreadable once
      # journald has line-wrapped it, and it is pure noise in
      # `docker logs`. The SSID is still logged as text; the passphrase
      # is NOT, as of upstream 2026.10.4 — it says "the passphrase is in
      # the join QR" and gates the plaintext behind --log-passphrase.
      # Deliberately left off: /hotspot renders both from the QR payload
      # on the shared volume, which is the path an operator actually
      # uses, and a WPA passphrase in journald is worth avoiding.
      - WIFI_CONNECT_NO_QR_STDOUT=true
      # Bring the portal down after 15 minutes with no successful join
      # and exit. This is the safety valve: if NetworkManager ever
      # misreports a working network as offline (its connectivity check
      # needs to reach network-test.debian.org, which a filtered
      # corporate LAN may block), the portal has taken a working device
      # off the air. A finite timeout bounds that to one window instead
      # of forever.
      # IDLE, not a cap on the session, as of wifi-connect 2026.10.9:
      # the clock restarts whenever someone loads a page or submits a
      # join, so an installer typing a passphrase at minute 14 is not
      # cut off -- which used to cost them the credentials too, since
      # the next run generates a new passphrase and a new SSID suffix.
      # Captive-portal probes do not count, so a phone left associated
      # cannot hold a screen in setup mode.
      - ACTIVITY_TIMEOUT=900
    # Deliberately NOT `restart: always`. wifi-connect is a one-shot:
    # it exits 0 the moment it finds the device online, so a restart
    # policy turns into a container start every 60s forever, and — far
    # worse — it would re-raise the portal every 15 minutes on a device
    # whose connectivity check is blocked, which is an outage that never
    # ends. One run per boot is upstream's own systemd semantic;
    # anthias-wifi-connect.service (ansible network role) is what
    # starts that run on each boot.
    restart: "no"
    # The safety gate lives in anthias-wifi-connect.service's
    # ExecCondition, and a plain `docker compose up -d` would walk
    # straight past it: `restart: "no"` governs restart-on-exit, not
    # the initial start. Without this profile, every run of
    # upgrade_containers.sh would raise an access point on a device
    # that is online — losing a WiFi-connected screen its network for
    # the length of a portal session, on every upgrade.
    #
    # A profiled service is skipped by `up -d` entirely: not started,
    # not created, and NOT removed by `--remove-orphans` once it
    # exists (verified against docker compose v5.6). So
    # upgrade_containers.sh creates it stopped, by name, and the
    # systemd unit is the only thing that ever starts it.
    profiles:
      - wifi-connect
    volumes:
      # Carries the QR, the state file AND the portal's branding: the
      # branding is staged here by anthias-server's startup script, so
      # there is nothing host-side to install and both platforms get the
      # same portal.
      - resin-data:/data
      - type: bind
        source: /run/dbus/system_bus_socket
        target: /run/dbus/system_bus_socket
    logging:
      driver: journald
      options:
        tag: anthias-wifi-connect

  redis:
    image: ghcr.io/screenly/anthias-redis:${DOCKER_TAG}-${DEVICE_TYPE}
    ulimits: *no-core-dumps
    build:
      context: .
      dockerfile: docker/Dockerfile.redis
    ports:
      - 127.0.0.1:6379:6379
    # Lets the depends_on conditions above hold service startup until
    # redis actually answers, covering the RDB-load window after a
    # restart on a slow SD card. start_period keeps the early probes
    # from counting against the retry budget.
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 12
      start_period: 10s
    restart: always
    volumes:
      - redis-data:/var/lib/redis
    logging:
      driver: journald
      options:
        tag: anthias-redis

volumes:
  resin-data:
  redis-data: