Dockerfile
Rama master · réplica de lectura del repositorio original.
# ═══════════════════════════════════════════════════════════════
# Sublarr — Multi-Stage Docker Build
# Stage 1: Build React Frontend (native platform for speed)
# Stage 2: Python Backend + Frontend Bundle
# ═══════════════════════════════════════════════════════════════
# Validate backend sources before they can enter any runtime image layer.
# This runs on the build host's architecture, including multi-arch builds.
FROM --platform=$BUILDPLATFORM python:3.12-slim AS backend-source
WORKDIR /source
COPY backend/ .
COPY docker/verify-source.py /verify-source.py
RUN python /verify-source.py /source
# Stage 1: Build React Frontend
FROM --platform=$BUILDPLATFORM node:26-alpine AS frontend
WORKDIR /build
COPY frontend/package*.json ./
RUN npm install --legacy-peer-deps
COPY frontend/ .
# Pre-compress the bundle (brotli+gzip) after the vite build so the backend can
# serve .br/.gz with Content-Encoding — big first-load win on low-power hosts.
# (Kept as vite build + compress rather than `npm run build` to skip the tsc
# type-check in the image; typecheck runs in CI/pre-deploy instead.)
RUN npx vite build && node scripts/compress-dist.mjs
# Stage 2: Python Backend + Frontend Bundle
FROM python:3.12-slim
# Pass from build: docker build --build-arg VERSION=$(cat backend/VERSION) ...
# Or use scripts/docker-build.sh to get version suggestions and build.
ARG VERSION=
LABEL org.opencontainers.image.title="Sublarr"
LABEL org.opencontainers.image.description="Standalone Subtitle Manager & Translator for Anime/Media"
LABEL org.opencontainers.image.source="https://github.com/Abrechen2/sublarr"
LABEL org.opencontainers.image.licenses="GPL-3.0"
LABEL org.opencontainers.image.version="${VERSION}"
# Also expose it to the running app (backend/version.py) so an RC-tagged
# build (e.g. "1.6.5-rc.2") reports its actual tag instead of the plain
# backend/VERSION content, which never carries the -rc.N suffix.
ENV SUBLARR_VERSION="${VERSION}"
# The repo default is the relative "log/sublarr.log", which is right for a
# source checkout and wrong for a container: it resolves under WORKDIR, so the
# log lives inside the container layer and is gone on the next recreate — and
# both the Logs page and the support export then read a file that resets on
# every update. /config is the persistent volume, same as the database default.
ENV SUBLARR_LOG_FILE="/config/sublarr.log"
# Install system dependencies
# PostgreSQL clients 15, 16 and 17 from the PostgreSQL project's own archive
# (PGDG): database_backup_postgres picks pg_dump/pg_restore matching the
# server's major version. Debian's single default client was 17, and a backup
# it wrote of a PostgreSQL 16 server could not be restored into that server
# (SET transaction_timeout) — owner decision 2026-09-16 after the rc.4 VM test.
# tesseract-ocr for OCR functionality, hunspell for spell checking
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates postgresql-common && \
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
apt-get install -y --no-install-recommends \
ffmpeg \
mkvtoolnix \
curl \
gosu \
unrar-free \
postgresql-client-15 \
postgresql-client-16 \
postgresql-client-17 \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
hunspell \
hunspell-de-de \
hunspell-en-us \
libenchant-2-2 \
fonts-liberation && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies
# Includes psycopg2-binary (PostgreSQL) and redis/rq (Redis) for optional backends
# ffsubsync pulls webrtcvad which has no prebuilt wheels → gcc + python3-dev
# required during install. Build tools are purged afterwards to keep the
# runtime image slim.
COPY backend/requirements.txt .
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc g++ python3-dev && \
pip install --no-cache-dir -r requirements.txt && \
apt-get purge -y --auto-remove gcc g++ python3-dev && \
rm -rf /var/lib/apt/lists/*
# Copy backend
COPY --from=backend-source /source/ .
# Copy built frontend
COPY --from=frontend /build/dist ./static
# Provide fallback font for libass-wasm subtitle renderer.
# Liberation Sans (metric-compatible Arial replacement, SIL OFL licence) is
# installed via fonts-liberation above. The worker loads /default.woff2 at
# startup; a 404 here crashes the worker before any subtitle renders.
RUN cp /usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf ./static/default.woff2
# Create non-root user with configurable UID/GID
ARG PUID=1000
ARG PGID=1000
RUN groupadd -g ${PGID} sublarr && \
useradd -u ${PUID} -g ${PGID} -m -s /bin/bash sublarr
# Create config and backups directory
RUN mkdir -p /config/backups && \
chown -R sublarr:sublarr /app /config
# Runtime entrypoint: remaps the sublarr user to PUID/PGID, fixes /config
# ownership on the bind-mounted volume, then drops privileges via gosu.
# Runs as root so it can chown /config and remap the user; gunicorn itself
# runs unprivileged as the requested PUID/PGID.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh && \
chmod +x /usr/local/bin/docker-entrypoint.sh
# NOTE: container starts as root so the entrypoint can chown /config and
# remap the user. The application process is dropped to PUID:PGID via gosu.
USER root
EXPOSE 5765
VOLUME ["/config", "/media"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:5765/api/v1/health || exit 1
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# --timeout is gunicorn's worker LIVENESS ceiling, not a per-request limit. The
# APScheduler ticks run inside this worker, and the longest of them (`cleanup`,
# JobSpec.timeout_s=7200) walks the whole media library — on a ~750-season
# library that is routinely 18-51 min. At the old 300s the arbiter SIGKILLed the
# worker mid-tick (prod, 2026-08-04 03:56, 11 min into the cleanup run): the tick
# died before _tick_wrapper could write its scheduler_job_runs row, so the run
# left no history entry, no timeout row and no traceback -- it simply vanished.
# Keep this >= the largest JobSpec.timeout_s so the app's own timeout fires
# first and gets recorded properly.
#
# --access-logfile - writes one line per request to stdout (hence into
# `docker logs`). Without it gunicorn logs nothing per request, so a clean 4xx
# left no trace anywhere and a whole probe campaign was invisible from the
# inside (found 2026-09-03). The paired in-app metric (HTTP_REQUEST_TOTAL, wired
# in app.py) gives the aggregate; this gives the individual line with its status.
#
# --access-logformat replaces %(r)s (the full request line) with %(m)s %(U)s:
# method and URL PATH ONLY. The default format logs the query string, and the
# API key may travel as ?apikey= (auth.py) — so the default would write the
# key in clear text to every log line and every support bundle. %(U)s drops
# the query; the path alone is what a request trace needs.
# Each Socket.IO long poll holds a gthread for up to 25 seconds. Four threads
# allow four browser connections to starve every API request, even /health.
# Use the threaded Flask-SocketIO deployment recommendation (100 threads).
# Keep one process: the scheduler and Socket.IO session state are process-local.
CMD ["gunicorn", "--bind", "0.0.0.0:5765", "--worker-class", "gthread", "--workers", "1", "--threads", "100", "--timeout", "7200", "--graceful-timeout", "15", "--access-logfile", "-", "--access-logformat", "%(h)s %(t)s \"%(m)s %(U)s\" %(s)s %(b)s %(D)sus", "app:create_app()"]