# Single-container build: nginx (UI) + node (API) in one image, for deployments # (e.g. Unraid CA) where running two linked containers is more setup than it's worth. # The split two-container images (web/Dockerfile, server/Dockerfile) remain the # docker-compose default since they scale/update independently. # ---- build web ---- FROM node:20-bookworm-slim AS build-web WORKDIR /app COPY web/package.json ./ RUN npm install COPY web/tsconfig.json web/vite.config.ts web/index.html ./ COPY web/public ./public COPY web/src ./src RUN npm run build # ---- build server ---- FROM node:20-bookworm-slim AS build-server WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* # The lockfile pins what CI tested; a lockless `npm install` re-resolves the whole tree and, since October 2026, crashes npm 10 ("Cannot read properties of null (reading 'edgesOut')"). COPY server/package.json server/package-lock.json ./ RUN npm ci COPY server/tsconfig.json ./ COPY server/src ./src RUN npm run build RUN cp src/db/schema.sql dist/db/schema.sql && cp src/db/schema.postgres.sql dist/db/schema.postgres.sql # ---- final ---- FROM node:20-bookworm-slim WORKDIR /app # unrar and 7z unpack RAR/7z downloads (server/src/services/archiveExtract.ts; ZIP is handled in # Node). unrar is freeware that permits redistribution but lives in Debian's non-free component, # so that is enabled first — in the deb822 debian.sources this image ships, or a classic sources.list. RUN for f in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do \ [ -f "$f" ] || continue; \ sed -i -E -e '/^Components:/{/[[:space:]]non-free([[:space:]]|$)/!s/$/ non-free/}' \ -e '/^deb[[:space:]]/{/[[:space:]]non-free([[:space:]]|$)/!s/$/ non-free/}' "$f"; \ done \ && apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip make g++ ca-certificates curl ffmpeg nginx gnupg p7zip-full unrar \ && rm -rf /var/lib/apt/lists/* \ && rm -f /etc/nginx/sites-enabled/default \ # Debian's nginx package (unlike the official nginx Docker image, which does this at build # time) logs to real files under /var/log/nginx by default — those never reach `docker logs`, # so every nginx-side request/error was completely invisible in Unraid's own container log # viewer even though the Node app's own console.log output showed up fine right next to it. # Symlinking to the standard streams, same trick the official image uses, merges nginx's own # logging into the container's stdout/stderr alongside the app's. && ln -sf /dev/stdout /var/log/nginx/access.log \ && ln -sf /dev/stderr /var/log/nginx/error.log # ffsubsync re-aligns a downloaded subtitle's timing against the video's own audio (voice-activity # detection, no external API) — used as a best-effort pass on subtitles the picker didn't already # consider a high-confidence match (see subtitleSync.ts). --break-system-packages is required on # Debian bookworm's externally-managed system Python; acceptable here since this container has no # other Python workload to conflict with. RUN pip install --no-cache-dir --break-system-packages ffsubsync # postgresql-client from Debian bookworm is v15, which refuses to pg_dump a newer (16/17) server — # pulling the latest client from PGDG's own repo instead, since a newer pg_dump can dump any older # server but not vice versa. RUN install -d /usr/share/postgresql-common/pgdg \ && curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc \ && . /etc/os-release \ && echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ && apt-get update && apt-get install -y --no-install-recommends postgresql-client-17 \ && rm -rf /var/lib/apt/lists/* # yt-dlp is fetched as a standalone binary (not via pip) to avoid pulling in a whole Python # packaging toolchain just for one tool — used by the "ytdlp" download client type. RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp \ && chmod +x /usr/local/bin/yt-dlp # gosu lets the entrypoint start as root (needed to chown /config and create a matching # user/group for PUID/PGID) and then drop the node process to that user before exec'ing it. RUN GOSU_ARCH="$(dpkg --print-architecture)" \ && curl -L "https://github.com/tianon/gosu/releases/download/1.17/gosu-${GOSU_ARCH}" -o /usr/local/bin/gosu \ && chmod +x /usr/local/bin/gosu ARG BUILD_TIME="" ARG GIT_COMMIT="" ENV NODE_ENV=production # A file call stuck on a dead mount holds one libuv thread for as long as the mount hangs; the default of 4 # would let a few of them stall DNS, async fs and zlib for the whole process. ENV UV_THREADPOOL_SIZE=16 ENV PORT=8989 ENV AONARR_CONFIG_DIR=/config ENV AONARR_DOWNLOADS_DIR=/downloads ENV AONARR_BUILD_TIME=$BUILD_TIME ENV AONARR_COMMIT=$GIT_COMMIT ENV AONARR_IMAGE_TAG=combined ENV PUID=99 ENV PGID=100 COPY server/package.json server/package-lock.json ./ RUN npm ci --omit=dev COPY --from=build-server /app/dist ./dist COPY CHANGELOG.md ./ # Without --build-arg BUILD_TIME the image still gets a build time (config.ts reads this file): the update # check and the database copy taken before migrations are keyed on it. RUN if [ -z "$BUILD_TIME" ]; then date -u +%Y-%m-%dT%H:%M:%SZ > /app/.build-time; fi COPY --from=build-web /app/dist /usr/share/nginx/html COPY combined/nginx.conf /etc/nginx/conf.d/aonarr.conf COPY combined/entrypoint.sh /entrypoint.sh # Optional URL_BASE (a sub-path such as /aonarr) is applied by this script at every start, so no rebuild is needed; # the same script serves the split web image (web/Dockerfile). COPY web/url-base.sh /usr/local/bin/aonarr-url-base RUN chmod +x /entrypoint.sh /usr/local/bin/aonarr-url-base VOLUME ["/config"] EXPOSE 9876 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD curl -f "http://127.0.0.1:9876$(cat /var/lib/aonarr-url-base/base 2>/dev/null)/api/health" || exit 1 ENTRYPOINT ["/entrypoint.sh"]