# Build stage: install the package under /out for the runtime stage to copy, so # pip and the source never reach the image. FROM alpine:3.24 AS build RUN apk add --no-cache python3 py3-pip WORKDIR /build COPY pyproject.toml README.md LICENSE ./ COPY src ./src # --break-system-packages: the install lands under /out, not the system tree. # --no-compile: the /out paths would be baked into the bytecode. --only-binary: # a cryptography release missing its musllinux wheel must fail here, not fall # back to building Rust. RUN pip install --no-cache-dir --break-system-packages --no-compile \ --only-binary=cryptography --root=/out --prefix=/usr . # Web build stage: Node exists only here. The UI compiles to static files. FROM node:26-alpine AS web WORKDIR /build/web COPY web/package.json web/package-lock.json ./ RUN npm ci COPY web ./ # The build reads the version out of it. See web/vite.config.ts. COPY src/trackstarr/__init__.py ../src/trackstarr/__init__.py ARG WEB_MODE=production RUN npm run build -- --mode "$WEB_MODE" # Optional static demo, built with --target demo --build-arg WEB_MODE=demo. FROM nginx:alpine AS demo COPY --from=web /build/web/build /usr/share/nginx/html COPY tools/demo/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 8080 HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \ CMD wget -q --spider http://127.0.0.1:8080/ || exit 1 # Alpine's ffmpeg carries the native AAC encoder, which is all the downmixes # need. No hardware acceleration: the work is stream copies and is disk-bound. # mkvtoolnix is the command-line package, for editing a track's tags in place. FROM alpine:3.24 RUN apk add --no-cache ffmpeg mkvtoolnix python3 tzdata COPY --from=build /out / COPY --from=web /build/web/build /web RUN python3 -m compileall -q /usr/lib/python3*/site-packages/trackstarr ENV PYTHONUNBUFFERED=1 # Where the web stage landed the UI; WEB_DIR= turns the pages off. ENV WEB_DIR=/web EXPOSE 5120 # So a bare `docker run` does not run as root and write root-owned files the # next non-root start cannot read. A numeric id needs no passwd entry, and # `--user` still wins. USER 1000:1000 # Only serve opens a port, so only serve is probed; a one-shot `sweep` would # otherwise sit at unhealthy for a restarter to act on. LISTEN_PORT rather # than a literal, or moving the listener leaves the container unhealthy. HEALTHCHECK --interval=60s --timeout=10s --start-period=15s --retries=3 \ CMD tr '\0' '\n' < /proc/1/cmdline | grep -qx serve || exit 0; \ wget -q --spider -T 5 "http://127.0.0.1:${LISTEN_PORT:-5120}/health" || exit 1 ENTRYPOINT ["trackstarr"] CMD ["serve"]