/
/
/
1# syntax=docker/dockerfile:1
2
3ARG BASE_IMAGE_VERSION=latest
4FROM --platform=$BUILDPLATFORM ghcr.io/music-assistant/base:$BASE_IMAGE_VERSION AS cliairplay-download
5
6# Bump the version and checksum-manifest hash together.
7ARG CLIAIRPLAY_VERSION=v0.5.3
8ARG CLIAIRPLAY_CHECKSUMS_SHA256=fdbc76b0ac9fe0020ddcc5939cd62e4af5e7d78422337fecbaf912d9ff0131b0
9ARG TARGETARCH
10
11# Download the cliairplay release asset for this image architecture.
12RUN set -eu \
13 && case "$TARGETARCH" in \
14 amd64) CLIAIRPLAY_ARCH="x86_64" ;; \
15 arm64) CLIAIRPLAY_ARCH="aarch64" ;; \
16 *) echo "Unsupported cliairplay architecture: $TARGETARCH" >&2; exit 1 ;; \
17 esac \
18 && CLIAIRPLAY_BINARY="cliairplay-linux-${CLIAIRPLAY_ARCH}" \
19 && RELEASE_URL="https://github.com/music-assistant/airplay-cli/releases/download/${CLIAIRPLAY_VERSION}" \
20 && wget -q "${RELEASE_URL}/SHA256SUMS" -O /tmp/SHA256SUMS \
21 && wget -q "${RELEASE_URL}/${CLIAIRPLAY_BINARY}" -O "/tmp/${CLIAIRPLAY_BINARY}" \
22 && echo "${CLIAIRPLAY_CHECKSUMS_SHA256} /tmp/SHA256SUMS" | sha256sum --check - \
23 && mkdir -p /cliairplay \
24 && mv "/tmp/${CLIAIRPLAY_BINARY}" "/cliairplay/${CLIAIRPLAY_BINARY}" \
25 && awk -v filename="$CLIAIRPLAY_BINARY" \
26 '$2 == filename || $2 == "*" filename' \
27 /tmp/SHA256SUMS > /tmp/cliairplay.sha256 \
28 && test "$(wc -l < /tmp/cliairplay.sha256)" -eq 1 \
29 && (cd /cliairplay && sha256sum --check /tmp/cliairplay.sha256) \
30 && chmod 755 "/cliairplay/${CLIAIRPLAY_BINARY}" \
31 && rm /tmp/SHA256SUMS /tmp/cliairplay.sha256
32
33FROM scratch AS cliairplay
34COPY --from=cliairplay-download /cliairplay /cliairplay
35
36# Builder image. It builds the venv that will be copied to the final image
37#
38FROM ghcr.io/music-assistant/base:$BASE_IMAGE_VERSION AS builder
39ARG TARGETARCH
40
41ADD dist dist
42COPY requirements_all.txt .
43
44# miniaudio has no Linux arm64 wheels, so pyatv requires a source build there.
45# The compiler stays in this disposable builder stage and is not copied to the final image.
46# RUN if [ "$TARGETARCH" = "arm64" ]; then \
47# apt-get update && \
48# apt-get install -y --no-install-recommends gcc g++ && \
49# rm -rf /var/lib/apt/lists/*; \
50# fi
51
52# TODO: Remove git after aiodatalibchannel is installed from pypi
53RUN apt-get update && \
54 apt-get install -y --no-install-recommends git gcc g++ && \
55 rm -rf /var/lib/apt/lists/*
56
57# ensure UV is installed
58COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
59
60# create venv which will be copied to the final image
61ENV VIRTUAL_ENV=/app/venv
62RUN uv venv $VIRTUAL_ENV
63
64# pre-install ALL requirements into the venv
65# comes at a cost of a slightly larger image size but is faster to start
66# because we do not have to install dependencies at runtime
67# --index-strategy: allow PyPI packages when also using the PyTorch extra index
68# https://docs.astral.sh/uv/pip/compatibility/#packages-that-exist-on-multiple-indexes
69# Keep urllib3-future from hijacking the urllib3 namespace (see pyproject.toml).
70ENV URLLIB3_NO_OVERRIDE=1
71RUN uv pip install \
72 --index-strategy unsafe-best-match \
73 --no-binary urllib3-future \
74 -r requirements_all.txt
75
76# Install PyAV from pre-built wheel (built against system FFmpeg in base image)
77# First verify the wheel version matches what pip resolved to avoid version mismatch
78RUN REQUIRED_VERSION=$($VIRTUAL_ENV/bin/python -c "import importlib.metadata; print(importlib.metadata.version('av'))") && \
79 WHEEL_VERSION=$(ls /usr/local/share/pyav-wheels/av*.whl | grep -oP 'av-\K[0-9.]+') && \
80 if [ "$REQUIRED_VERSION" != "$WHEEL_VERSION" ]; then \
81 echo "ERROR: PyAV version mismatch! Requirements need $REQUIRED_VERSION but base image has $WHEEL_VERSION" && \
82 echo "Please rebuild the base image with the correct PyAV version." && \
83 exit 1; \
84 fi && \
85 uv pip install --force-reinstall --no-deps /usr/local/share/pyav-wheels/av*.whl
86
87# Install Music Assistant from prebuilt wheel
88ARG MASS_VERSION
89RUN uv pip install \
90 --no-cache \
91 "music-assistant@dist/music_assistant-${MASS_VERSION}-py3-none-any.whl"
92
93COPY --from=cliairplay /cliairplay /tmp/cliairplay
94RUN SITE_PACKAGES="$("$VIRTUAL_ENV/bin/python" -c \
95 'import sysconfig; print(sysconfig.get_path("purelib"))')" \
96 && CLIAIRPLAY_BIN_DIR="${SITE_PACKAGES}/music_assistant/providers/airplay/bin" \
97 && mkdir -p "$CLIAIRPLAY_BIN_DIR" \
98 && mv /tmp/cliairplay/* "$CLIAIRPLAY_BIN_DIR/" \
99 && rmdir /tmp/cliairplay \
100 && "$CLIAIRPLAY_BIN_DIR"/cliairplay-linux-* --check
101
102# Pre-compile Python bytecode for faster startup
103RUN $VIRTUAL_ENV/bin/python -m compileall -q $VIRTUAL_ENV/lib/python*/site-packages/music_assistant
104
105# we need to set (very permissive) permissions to the workdir
106# and /tmp to allow running the container as non-root
107# IMPORTANT: chmod here, NOT on the final image, to avoid creating extra layers and increase size!
108#
109RUN chmod -R 777 /app \
110 && chmod 755 \
111 "$VIRTUAL_ENV"/lib/python*/site-packages/music_assistant/providers/airplay/bin/cliairplay-linux-*
112
113##################################################################################################
114
115# FINAL docker image for music assistant server
116
117FROM ghcr.io/music-assistant/base:$BASE_IMAGE_VERSION
118
119ENV VIRTUAL_ENV=/app/venv
120ENV PATH="$VIRTUAL_ENV/bin:$PATH"
121
122# copy the already built /app dir
123COPY --from=builder /app /app
124
125# the /app contents have correct permissions but for some reason /app itself does not.
126# so apply again, but ONLY to the dir (otherwise we increase the size)
127RUN chmod 777 /app
128
129# Set some labels
130ARG MASS_VERSION
131ARG TARGETPLATFORM
132LABEL \
133 org.opencontainers.image.title="Music Assistant Server" \
134 org.opencontainers.image.description="Music Assistant is a free, opensource Media library manager that connects to your streaming services and a wide range of connected speakers. The server is the beating heart, the core of Music Assistant and must run on an always-on device like a Raspberry Pi, a NAS or an Intel NUC or alike." \
135 org.opencontainers.image.source="https://github.com/music-assistant/server" \
136 org.opencontainers.image.authors="The Music Assistant Team" \
137 org.opencontainers.image.documentation="https://music-assistant.io" \
138 org.opencontainers.image.licenses="Apache License 2.0" \
139 io.hass.version="${MASS_VERSION}" \
140 io.hass.type="addon" \
141 io.hass.name="Music Assistant Server" \
142 io.hass.description="Music Assistant Server" \
143 io.hass.platform="${TARGETPLATFORM}" \
144 io.hass.type="addon"
145
146VOLUME [ "/data" ]
147EXPOSE 8095
148
149WORKDIR $VIRTUAL_ENV
150
151# Entrypoint script that enables jemalloc for the main process only.
152# MALLOC_CONF enables jemalloc's background thread so freed pages are returned to
153# the OS while the process is idle; without it an idle server holds onto the peak
154# RSS reached during startup (db migration, provider setup, metadata, image decode).
155RUN printf '#!/bin/sh\n\
156for path in /usr/lib/*/libjemalloc.so.2; do\n\
157 [ -f "$path" ] && export LD_PRELOAD="$path" MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000" && break\n\
158done\n\
159exec mass "$@"\n' > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
160
161ENTRYPOINT ["/usr/local/bin/entrypoint.sh", "--data-dir", "/data", "--cache-dir", "/data/.cache"]
162