# Radio Conjurer - Liquidsoap runtime built through opam, so the OCaml, # opam and Liquidsoap versions are all selectable at build time. # # Build from the repository root, e.g.: # docker build -f docker/Dockerfile.radio -t conjurer-radio . # docker build -f docker/Dockerfile.radio -t conjurer-radio \ # --build-arg LIQUIDSOAP_VERSION=2.1.4 --build-arg OCAML_VERSION=4.14.2 . # # Version notes (matched to radio_conjurer.liq, which targets 2.1.4): # * Liquidsoap 2.1.x requires OCaml 4.x - it does NOT build on OCaml 5. # Production ran the 4.13.0 switch; 4.14.x is the maintained 4.x line and # builds 2.1.4 fine. Pass OCAML_VERSION=4.13.0 for bit-for-bit parity. # * The ocaml-ffmpeg bindings compatible with 2.1.4 target the FFmpeg 5.x # library family (libavcodec59/libavformat59/libavutil57...). Debian # bookworm ships exactly that, which is why this image needs NO ffmpeg # pinning: the old ffmpeg.pref (Pin-Priority 1001 on deb.debian.org) only # existed to force those Debian builds over the Raspberry Pi OS repo's # conflicting ones. Single-repo container = the pin is redundant. FROM debian:bookworm-slim ARG OPAM_VERSION=2.1.5 ARG OCAML_VERSION=4.14.2 ARG LIQUIDSOAP_VERSION=2.1.4 # Optional liquidsoap features, resolved together with liquidsoap by opam. # These cover everything radio_conjurer.liq uses: # mad+lame - mp3 decode/encode (%mp3 icecast stream) # cry - output.icecast # taglib - tag/replaygain metadata # pulseaudio- input.pulseaudio (mic) + output.pulseaudio # samplerate- resampling # inotify - playlist(reload_mode="watch") # ffmpeg - decode fallback + replaygain computation ARG LIQ_OPAM_PACKAGES="mad lame cry taglib pulseaudio samplerate inotify ffmpeg" # System libraries: build deps for the opam packages above plus the matching # runtime libs. The libav*-dev list mirrors the old ffmpeg.pref family. RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential m4 pkg-config git curl ca-certificates unzip rsync \ libpcre3-dev libgmp-dev zlib1g-dev \ libmad0-dev libmp3lame-dev libtag1-dev \ libpulse-dev libsamplerate0-dev \ libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev \ libavdevice-dev libswresample-dev libswscale-dev libpostproc-dev \ libcurl4-gnutls-dev \ ffmpeg \ pulseaudio pulseaudio-utils \ icecast2 jq \ python3 python3-flask python3-waitress python3-requests \ && rm -rf /var/lib/apt/lists/* # Liquidsoap refuses to run as root (security exit), so it gets a dedicated # user. audio/pulse-access mirror what was done by hand on the Pi for the # system-wide pulse socket ('pulse-access' comes with the pulseaudio pkg). RUN useradd --system --create-home --home-dir /var/lib/radio \ --shell /usr/sbin/nologin radio \ && usermod -aG audio,pulse-access radio # opam as a static binary so OPAM_VERSION is a real choice (apt would pin us # to whatever bookworm ships). RUN ARCH=$(uname -m) \ && curl -fsSL -o /usr/local/bin/opam \ "https://github.com/ocaml/opam/releases/download/${OPAM_VERSION}/opam-${OPAM_VERSION}-${ARCH}-linux" \ && chmod +x /usr/local/bin/opam # OCaml switch + liquidsoap and its optional feature libraries in one solve, # so liquidsoap is compiled WITH those features enabled. OPAMROOT lives in # /opt/opam (not /root/.opam) so the unprivileged 'radio' user can read the # liquidsoap binary AND its stdlib .liq files at runtime. ENV OPAMROOT=/opt/opam RUN opam init -y --bare --disable-sandboxing \ && opam switch create default "${OCAML_VERSION}" \ && opam install -y "liquidsoap.${LIQUIDSOAP_VERSION}" ${LIQ_OPAM_PACKAGES} \ && opam clean -a -c -s --logs ENV PATH="/opt/opam/default/bin:${PATH}" # Build-time smoke test: the binary runs and reports the requested version. RUN liquidsoap --version # Minimal system-wide pulse config for headless VMs (PULSE_MODE=internal): # a null sink so output.pulseaudio()/input.pulseaudio() work with no sound # hardware (mic becomes silence, which blank.strip already gates out). COPY docker/pulse-system.pa /etc/pulse/system.pa # The script and its persistent params are seeded into the data volume on # first run (never overwritten), so live edits survive image rebuilds. The # icecast config is rendered from the template at startup with passwords # taken from the secrets volume (never baked into the image). WORKDIR /app COPY conjurer_musician/radio_conjurer.liq ./radio_conjurer.liq COPY conjurer_musician/script.params ./script.params COPY conjurer_musician/stream.html ./stream.html COPY conjurer_betoniarka/betoniarka.py ./betoniarka.py COPY docker/icecast.xml.tpl ./icecast.xml.tpl COPY docker/entrypoint.radio.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Volume layout matches the paths inside radio_conjurer.liq: # /srv/betoniarka/data - playlists, script.params, persistence/radio logs # /srv/betoniarka/music - the mp3 library # /srv/betoniarka/secrets - icecast_credentials.json (provisioned at install) VOLUME ["/srv/betoniarka/data", "/srv/betoniarka/music", "/srv/betoniarka/secrets"] # 8000 = icecast (listeners), 5005 = betoniarka HTTP API (the bot's # CONJURER_RADIO_SERVICE), 54321 = harbor /skip (the bot's RADIO_HARBOR), # 9999 = interactive harbor. EXPOSE 8000 5005 54321 9999 ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["liquidsoap", "/srv/betoniarka/data/radio_conjurer.liq"]