Dockerfile.combined
Rama main · réplica de lectura del repositorio original.
# 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"]