Ga naar inhoud

Installation

Deze inhoud is nog niet vertaald.

Castmux is distributed as a container image and this is the supported way to run it. The image contains the Go binary and both streaming engines, along with a small set of command-line tools the process shells out to.

Terminal window
mkdir -p /opt/castmux/config
docker run -d --name castmux --restart unless-stopped \
--network host \
--cap-add NET_RAW \
--cap-add NET_ADMIN \
-v /opt/castmux/config:/config \
-e CASTMUX_WEB_USERNAME=admin \
-e CASTMUX_WEB_PASSWORD=choose-something-better \
castmux/castmux:latest

Or as a compose file:

/opt/castmux/docker-compose.yml
services:
castmux:
image: castmux/castmux:latest
container_name: castmux
restart: unless-stopped
# Multicast must be emitted onto the real LAN, not a bridge.
network_mode: host
cap_add:
- NET_RAW # libpcap capture: viewer detection, wire bitrate
- NET_ADMIN # multicast route inspection and management
volumes:
# A host directory, not a named volume — see below.
- /opt/castmux/config:/config
environment:
CASTMUX_WEB_USERNAME: admin
CASTMUX_WEB_PASSWORD: choose-something-better

A Docker bridge network is a separate layer 2 segment from the one the televisions are on. Multicast sent into it is delivered within the bridge and nowhere else. The failure mode is unusually unhelpful: the send call succeeds, the engine reports it is streaming, the packet counters increase, and no television ever receives anything. There is no error to find because no error occurred.

Host networking is therefore not a convenience, it is a functional requirement. It also means the web interface binds directly to port 8080 on the host, so no port mapping is needed or possible.

Castmux captures packets with libpcap in promiscuous mode. It does this for two things that cannot be done any other way:

  • Viewer detection. IGMP membership reports from other hosts are addressed to groups this machine has not joined, and the kernel discards non-joined multicast before any ordinary socket can see it. Promiscuous capture is the only way to observe which televisions have joined which group.
  • Wire bitrate. Counting what actually left the interface, rather than believing what the engine says it sent. This is the only liveness signal that works identically for both engines and the only one a confused engine cannot fake.

Without NET_RAW, on-demand streaming has nothing to act on and the UDP bitrate tiles stay empty. Whether channels then run always-on or not at all depends on the fail-open setting described in On-demand streaming.

Multicast route management and inspection. Castmux checks the host routing table from outside the streaming path, because — as the next section explains — an unroutable destination is completely invisible from inside a process that is sending to it.

All state lives in one file: /config/config.yaml inside the container, which is wherever you bind-mounted it on the host.

Bind-mount a host directory rather than using a named volume. This is a deliberate recommendation, not an oversight. The moment you most need to read the configuration is the moment the web interface is not answering, and at that moment a named volume is a directory buried under the container runtime’s storage root with a name nobody can remember. A plain path means the configuration can be read with an editor, diffed against last week’s, backed up with cp, and put into version control if you want a change history.

On first run the file is created with defaults. Thereafter:

  • Missing keys are backfilled automatically, so a configuration written by an older version keeps working after an upgrade and gains the new keys at their defaults.
  • Before any migration that changes the file’s structure, a .pre-migration copy is written alongside it. If an upgrade interprets something differently from how you intended, the original is still there.

Editing the file by hand is supported. The process reads it at start-up, so restart the container after an out-of-band edit, and be aware that changes made in the web interface while you are editing will be overwritten by whichever write lands last.

This is the part installations get wrong, and it is worth more attention than the rest of this page combined.

The kernel decides which interface a multicast packet leaves by consulting the routing table. Binding the source address of the UDP socket does not select the egress interface — a common and reasonable assumption, and an incorrect one. What matters is where 224.0.0.0/4 points.

On many appliances it points at whichever interface the installer happened to configure first, which is frequently one that is not plugged in. Streams are then produced correctly and silently go nowhere.

Route the whole multicast range at the interface facing the televisions:

Terminal window
# Point the entire multicast range at the TV-facing NIC
ip route replace 224.0.0.0/4 dev eth1
# Verify — this must name the TV-facing interface
ip route get 239.10.0.1

An ip route command is not persistent. Install the provided systemd unit and its accompanying timer instead. The unit:

  • picks the first connected candidate interface, preferring the television-facing NIC and falling back to the management NIC when the preferred one has no carrier;
  • is re-evaluated by the timer every minute, so the route follows the cable if somebody moves it rather than needing a reboot to notice.

sendto() to an unroutable multicast destination returns success. No streaming engine can detect this condition, because from the engine’s point of view nothing went wrong. It follows that:

  • the fault must be checked against the routing table from outside the streaming path, which is what Castmux does and why it needs NET_ADMIN;
  • restarting never fixes it. Both the application and the watchdog therefore refuse to restart a channel for a routing fault, and raise an alert instead. A restart loop against a broken route is churn that hides the actual cause.

The image declares a container healthcheck. Be aware of what that does and does not buy you: plain Docker records the health status and takes no action on it. A container can sit marked unhealthy indefinitely while the restart policy, which only reacts to the process exiting, does nothing. Orchestrators act on health; docker run --restart does not.

An external watchdog unit therefore does the acting. It takes two independent signals:

SignalWhat it means and what happens
Container healthThe process as a whole is not answering. The response is to restart the container.
Per-channel UDP silenceOne channel has stopped putting packets on the wire while the rest are fine. The response is to restart that channel only.

Two signals rather than one, because they distinguish faults with very different blast radii. Restarting the whole container to fix one stuck channel takes every other channel off air for the duration of the restart and the staggered start that follows, which on twenty channels is about a minute of black screens to fix a problem affecting one.

The watchdog refuses to restart anything for a routing fault, for the reason given above: a restart cannot fix a route, so restarting produces a loop instead of a repair.

Two situations produce a second instance without anyone intending one:

  • A leftover systemd unit from a previous non-container install. Disabling a unit stops it starting at boot; it does not stop anything else starting it. Use systemctl mask so that it cannot be started at all.
  • A nightly restart in root’s crontab. Check for one when migrating an appliance from a package install to the container. A cron entry running systemctl restart on the old unit will happily start a disabled unit at three in the morning, giving you a split brain that appears overnight and is gone by the time anyone investigates. Again: mask, do not merely disable.

Pull the new image and recreate the container. The configuration migrates forward automatically, missing keys are backfilled, and a .pre-migration backup is written before any structural change.

Terminal window
docker pull castmux/castmux:latest
docker stop castmux && docker rm castmux
# then re-run the same docker run command, or:
docker compose up -d

Channels are down for the duration of the restart plus the staggered start that follows — roughly three seconds per channel. Plan the window accordingly on a large installation.

Migration is forward-only. If you need to go back to an older image, restore the .pre-migration copy of the configuration alongside it; an older binary reading a newer file will not understand keys that did not exist when it was built.

Copy the configuration directory. That is the whole procedure, and it is short on purpose:

Terminal window
cp -a /opt/castmux/config /backup/castmux-$(date +%F)

There is no database, no runtime state worth preserving and nothing else to capture. Restoring is the same operation in reverse followed by a container start. Channel logs are not included, because they are a memory-resident circular buffer and do not persist across a restart in any case — see The web interface → Logs.