Ctrl K
ComunidadGuías…

Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Stage 1: FRONTEND BUILD
# ------------------------------------------------------------------------------------------
# Docker Hardened Image (DHI) Debian 13 base with Socket Firewall pre-installed
# to protect build environment from malicious dependencies.
# Ref: https://hub.docker.com/hardened-images/catalog/dhi/node
# Constraint: Node 26 is not yet available in DHI, fallback to LTS Node 24 (supported until 2027).
# Note: newer DHI Node 24 digests have dropped pre-installed pnpm (verified by
# failed build during v0.8.2). Stay pinned to this digest until DHI restores
# pnpm in the image, or until we add an explicit corepack/pnpm install step.
# digest-refresh: skip
FROM dhi.io/node:24-debian13-sfw-dev@sha256:d33e9108a3a7ef728ee61f90a951dce680433a768a9a09134fd721b10f8b110b AS frontend-build-stage

ENV NODE_ENV=production

WORKDIR /app
COPY frontend/ ./frontend

WORKDIR /app/frontend
# Note: Hardened Node image pre-installs pnpm.
# Intent: Use BuildKit cache mount for the pnpm global store to speed up rebuilds
# when package.json is modified.
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm config set store-dir /pnpm/store && \
    CI=true pnpm install --frozen-lockfile
# Metadata changes only the final frontend layer, keeping dependency caches warm.
ARG BUILD_COMMIT=""
ARG BUILD_DATE=""
ARG BUILD_REF=""
ARG BUILD_VERSION=""
ARG BUILD_PRERELEASE="false"
RUN BUILD_COMMIT="$BUILD_COMMIT" BUILD_DATE="$BUILD_DATE" BUILD_REF="$BUILD_REF" \
    BUILD_VERSION="$BUILD_VERSION" BUILD_PRERELEASE="$BUILD_PRERELEASE" \
    node scripts/write-build-info.mjs && pnpm run build

# The built static files are in /app/frontend/out/

# Stage 2: PYTHON BACKEND BUILD
# ------------------------------------------------------------------------------------------
# Intent: Fallback to debian-base:trixie-debian13-dev because dhi.io/python:3.11-debian13 is 
# currently affected by CVE-2026-6100 (CVSS 9.1) without an upstream patch.
# Ref: https://scout.docker.com/vulnerabilities/id/CVE-2026-6100
FROM dhi.io/debian-base:trixie-debian13-dev@sha256:94571f3dc07fbc7166f6f1cbf1fe068a7647198ee2554e6b90d59a68cc8b053c AS backend-build-stage

# Use 'uv' for high-performance Python package management instead of standard pip.
# Ref: https://github.com/astral-sh/uv
COPY --from=dhi.io/uv:0.11.31-debian13@sha256:a39297c8ffc840971da90952aec9123d991bd007a0402edb2fd814421506d622 /uv /uvx /bin/

# 🧩 Install system dependencies required for full Pillow image format support
#
# This layer installs libraries that enable reading/writing many image formats:
#   - libjpeg, libpng, libtiff, libwebp, libopenjp2: common raster formats (JPEG, PNG, TIFF, WebP, JPEG2000)
#   - libimagequant: high-quality PNG quantization
#   - libheif: enables HEIF / HEIC / AVIF image decoding
#   - ghostscript: enables reading vector formats like .EPS, .PS, and .PDF
#   - libcairo: SVG rasterization through CairoSVG
#   - liblcms2, libfreetype, libharfbuzz, libfribidi: color management + advanced text rendering
#   - libxcb, zlib, libgif: core compression and GIF/X11 support
#
# Together, these libraries ensure Pillow (PIL) can handle nearly every major image type used in production.
# But I haven't tested all in CI, yet.

# Feature: Docker cache mounts for faster builds.
# (apt doesn't need to resolve again after first successful run)
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
    echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache

# Workaround: BuildKit 'COPY' cannot dynamically resolve host-architecture triplet 
# paths (e.g. x86_64 vs aarch64). We export the matching directory to a predictable 
# path (/dpkg-export) to facilitate architecture-agnostic multi-arch copying later.
#
# Strategy: Runtime Closure Extractor (ldd + dpkg-L hybrid)
# Ref: extract_deps.sh
COPY scripts/extract_deps.sh /tmp/extract_deps.sh
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    set -eux; \
    \
    # DHI renamed its packages to +deb13uN+dhiX but still lists the old +dhiN
    # builds, which sort higher and now return 403. Prefer the renamed ones,
    # below 1000 so apt never downgrades what the base image ships.
    printf 'Package: /./\nPin: version /deb13u/\nPin-Priority: 990\n' > /etc/apt/preferences.d/dhi-deb13u; \
    # The renamed libgomp1 needs the renamed gcc-14 runtime, which sorts below the
    # one the base image ships, so move that package family together.
    printf 'Package: gcc-14-base libgcc-s1 libstdc++6 libgomp1\nPin: version /deb13u/\nPin-Priority: 1001\n' > /etc/apt/preferences.d/00-dhi-gcc-base; \
    \
    # Stub SysV init helpers so postinst scripts don't crash in a container.
    # x11-common (a ghostscript transitive dep) calls both update-rc.d and invoke-rc.d
    # in its postinst, but neither exists in a minimal image without sysvinit/openrc.
    printf '#!/bin/sh\nexit 0\n'   > /usr/sbin/update-rc.d && chmod +x /usr/sbin/update-rc.d; \
    printf '#!/bin/sh\nexit 0\n'   > /usr/sbin/invoke-rc.d && chmod +x /usr/sbin/invoke-rc.d; \
    printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d && chmod +x /usr/sbin/policy-rc.d; \
    \
    # The DHI apt mirror occasionally serves a Packages.gz whose size doesn't
    # match the hash in the still-cached Release file ("File has unexpected
    # size ... Mirror sync in progress?"). It self-heals within minutes, so
    # retry apt-get update with backoff instead of failing the whole build.
    i=0; \
    until apt-get update -o Acquire::Retries=5 -o Acquire::http::Timeout=30; do \
        i=$((i+1)); \
        if [ "$i" -ge 5 ]; then echo "apt-get update failed after 5 attempts" >&2; exit 1; fi; \
        echo "apt-get update transient failure (attempt $i/5), retrying in 15s..."; \
        sleep 15; \
    done && \
    apt-get install -y --no-install-recommends --allow-downgrades gcc-14-base libgcc-s1 \
        ghostscript libcairo2 \
        libjpeg62-turbo libpng16-16 libtiff6 libwebp7 libopenjp2-7 \
        libimagequant0 libheif1 liblcms2-2 \
        libfreetype6 libharfbuzz0b libfribidi0 \
        libxcb1 zlib1g libgif7 \
        dumb-init \
        libstdc++6 libgomp1 && \
    \
    # Phase 1 (ldd):   resolve .so paths from gs binary → copy directly by filesystem path.
    #                   Immune to Debian t64 renames (libpng16-16 → libpng16-16t64, etc.)
    # Phase 2 (dpkg-L): copy data files (CMaps, fonts, dumb-init binary) that ldd misses.
    # See extract_deps.sh for details.
    CAIRO_LIBRARY=$(dpkg -L libcairo2 | sed -n '/\/libcairo\.so\.2$/p') && \
    test -n "$CAIRO_LIBRARY" && \
    EXTRACT_DEPS_TARGET=/dpkg-export sh /tmp/extract_deps.sh "/usr/bin/gs $CAIRO_LIBRARY" && \
    cp --parents -a "$(realpath "$CAIRO_LIBRARY")" "$CAIRO_LIBRARY" /dpkg-export/ && \
    \
    # Generate ld.so.cache so the dynamic linker can find all .so files at runtime.
    # Without this, libgs.so.10 fails to load its device plugins ("Unable to open
    # the initial device") because the hardened runtime image has no ld.so.cache.
    ldconfig && cp --parents /etc/ld.so.cache /dpkg-export/

# Setup runtime directory for nonroot user (pre-configured in DHI, UID/GID 65532).
RUN mkdir -p /container && \
    chown -R nonroot:nonroot /container

USER nonroot

ENV VIRTUAL_ENV=/container/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Setup standalone Python managed by uv to avoid missing python in final stage.
# This standalone python is statically built and does not depend on OS libraries.
#
# Pinned to 3.14 rather than 3.11. Beyond being current, the standalone builds
# from 3.12 onwards no longer bundle setuptools and ship a patched pip, which is
# what removes the last vulnerabilities from the image:
#
#   3.11  pip 26.1.1 (CVE-2026-8643)  setuptools 82.0.1 (CVE-2026-59890)
#   3.14  pip 26.1.2                  setuptools not shipped
#
# Neither package is reachable at runtime in any case, since the venv is created
# without system site-packages, but the scanner reports what is present on disk.
ENV UV_PYTHON_INSTALL_DIR=/container/python
ENV PYTHON_VERSION=3.14
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,uid=65532,gid=65532 \
    uv python install "$PYTHON_VERSION" && \
    uv venv --python "$PYTHON_VERSION" $VIRTUAL_ENV

WORKDIR /container

COPY --chown=nonroot:nonroot requirements.txt .
COPY --chown=nonroot:nonroot setup.py .

RUN --mount=type=cache,target=/home/nonroot/.cache/uv,uid=65532,gid=65532 \
    uv pip install -r requirements.txt

COPY --chown=nonroot:nonroot backend/ ./backend

RUN --mount=type=cache,target=/home/nonroot/.cache/uv,uid=65532,gid=65532 \
    uv pip install .

# Pre-download rembg model to prevent download overhead during runtime.
ENV U2NET_HOME=/container/.u2net
# Intent: Since backend code is copied earlier, any code change invalidates layer cache.
# We use a BuildKit cache mount at /cache/u2net so the model is not re-downloaded 
# from the internet, then copy it to the persistent U2NET_HOME inside the image.
RUN --mount=type=cache,target=/cache/u2net,uid=65532,gid=65532 \
    U2NET_HOME=/cache/u2net python - <<'PY' && cp -a /cache/u2net/. /container/.u2net/
import time
from backend.image_converter.config import settings
from rembg import new_session
model_name = settings.get().rembg.model_name
# The model comes from GitHub releases, which sometimes time out in CI.
for attempt in range(1, 6):
    try:
        new_session(model_name)
        break
    except Exception as exc:
        if attempt == 5:
            raise
        print(f"rembg download failed (attempt {attempt}/5): {exc}; retrying in {attempt * 15}s", flush=True)
        time.sleep(attempt * 15)
print(f"rembg model cached: {model_name}")
PY

COPY --chown=nonroot:nonroot entrypoint.py ./entrypoint.py
COPY --chown=nonroot:nonroot healthcheck.py ./healthcheck.py

# Create static site directory. Required pre-creation as a nonroot user 
# to avoid permission issues when copying frontend assets.
RUN mkdir -p /container/backend/image_converter/presentation/web/static_site

# Stage 2b: AI UPSCALING MODEL BUILD
# ------------------------------------------------------------------------------------------
# Intent: Build the general (plus its two noise-reduction presets) and anime ONNX models from the official Real-ESRGAN weights in
# a stage of its own. The .pth weights are a Python pickle, so they are SHA-256 checked
# against the official release before anything loads them, then loaded with
# torch.load(weights_only=True) and exported to ONNX (graph and weights, no code).
# Only the .onnx files and their .sha256 files are copied into the final image; torch,
# onnx and the .pth files stay here. See scripts/build_upscale_model.py.
FROM dhi.io/debian-base:trixie-debian13-dev@sha256:c6fc0de84b65bc20346cee5f071fd976ccf383431515db2a4d9721ca936feb9f AS upscale-model-stage

COPY --from=dhi.io/uv:0.11.31-debian13@sha256:a39297c8ffc840971da90952aec9123d991bd007a0402edb2fd814421506d622 /uv /uvx /bin/

# torch and onnxruntime wheels link against the system C++ runtime and OpenMP, which
# the base image doesn't ship (same packages as in the backend stage).
# ca-certificates: the build script downloads the official weights over HTTPS with
# Python's own TLS stack, which finds no CA certificates in this image (uv and
# requests/certifi bring their own roots, urllib does not). Without it the download
# fails with CERTIFICATE_VERIFY_FAILED.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    set -eux; \
    # DHI renamed its packages to +deb13uN+dhiX but still lists the old +dhiN
    # builds, which sort higher and now return 403. Prefer the renamed ones,
    # below 1000 so apt never downgrades what the base image ships.
    printf 'Package: /./\nPin: version /deb13u/\nPin-Priority: 990\n' > /etc/apt/preferences.d/dhi-deb13u; \
    # The renamed libgomp1 needs the renamed gcc-14 runtime, which sorts below the
    # one the base image ships, so move that package family together.
    printf 'Package: gcc-14-base libgcc-s1 libstdc++6 libgomp1\nPin: version /deb13u/\nPin-Priority: 1001\n' > /etc/apt/preferences.d/00-dhi-gcc-base; \
    i=0; \
    until apt-get update -o Acquire::Retries=5 -o Acquire::http::Timeout=30; do \
        i=$((i+1)); \
        if [ "$i" -ge 5 ]; then echo "apt-get update failed after 5 attempts" >&2; exit 1; fi; \
        sleep 15; \
    done && \
    apt-get install -y --no-install-recommends --allow-downgrades ca-certificates gcc-14-base libgcc-s1 libstdc++6 libgomp1 && \
    mkdir -p /build && chown -R nonroot:nonroot /build

# uv's standalone Python looks for /etc/ssl/cert.pem, which Debian doesn't create;
# point it at the bundle ca-certificates installs.
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt

USER nonroot
WORKDIR /build

ENV UV_PYTHON_INSTALL_DIR=/build/python
# CPU-only torch wheel (no CUDA libraries); versions pinned so the exported graph is
# reproducible and matches the operator allowlist in the build script.
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,uid=65532,gid=65532 \
    uv python install 3.14 && \
    uv venv --python 3.14 /build/venv && \
    uv pip install --python /build/venv/bin/python \
        --index-url https://download.pytorch.org/whl/cpu torch==2.14.1 && \
    uv pip install --python /build/venv/bin/python \
        onnx==1.23.2 onnxruntime==1.26.0 numpy==2.5.3

COPY --chown=nonroot:nonroot scripts/build_upscale_model.py ./build_upscale_model.py
# The cache mount keeps the verified .pth between builds; it is re-checked every time.
RUN --mount=type=cache,target=/home/nonroot/.cache/upscale-weights,uid=65532,gid=65532 \
    /build/venv/bin/python build_upscale_model.py /build/models \
        --cache /home/nonroot/.cache/upscale-weights

# Stage 3: FINAL RUNTIME
# ------------------------------------------------------------------------------------------
FROM dhi.io/debian-base:trixie-debian13@sha256:5c9d8c2c90a0a4672f44a42a485c99e1b95f4bfad5c8ef49934f34b7ffb6e23b AS final-stage

LABEL org.opencontainers.image.authors="Karim Zouine <mails.karimzouine@gmail.com>" \
      org.opencontainers.image.vendor="Karim Zouine" \
      org.opencontainers.image.title="imgcompress - High Performance Image Compression & Background Removal" \
      org.opencontainers.image.description="Self-hosted, privacy-first tool for image compression, conversion (HEIC/WebP/PDF), background removal and upscaling using local AI. Supports 70+ formats." \
      org.opencontainers.image.url="https://github.com/karimz1/imgcompress" \
      org.opencontainers.image.source="https://github.com/karimz1/imgcompress" \
      org.opencontainers.image.documentation="https://github.com/karimz1/imgcompress" \
      org.opencontainers.image.licenses="GPL-3.0-or-later"

ENV VIRTUAL_ENV=/container/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV U2NET_HOME=/container/.u2net
ENV IMGCOMPRESS_MODEL_HOME=/container/.models

WORKDIR /container

COPY --from=backend-build-stage /dpkg-export/ /
COPY --from=backend-build-stage --chown=65532:65532 /container/python /container/python
COPY --from=backend-build-stage --chown=65532:65532 /container/venv /container/venv
COPY --from=backend-build-stage --chown=65532:65532 /container/.u2net /container/.u2net
COPY --from=upscale-model-stage --chown=65532:65532 /build/models /container/.models
COPY --from=backend-build-stage --chown=65532:65532 /container/backend/ /container/backend
COPY --from=backend-build-stage --chown=65532:65532 /container/entrypoint.py /container/entrypoint.py
COPY --from=backend-build-stage --chown=65532:65532 /container/healthcheck.py /container/healthcheck.py

COPY --from=frontend-build-stage --chown=65532:65532 /app/frontend/out/. \
    /container/backend/image_converter/presentation/web/static_site
COPY --from=frontend-build-stage --chown=65532:65532 /app/frontend/.next \
    /container/backend/image_converter/presentation/web/static_site
COPY --from=frontend-build-stage --chown=65532:65532 /app/frontend/public \
    /container/backend/image_converter/presentation/web/static_site

USER nonroot

EXPOSE 5000

# Check the Cairo shared-library closure in the shell-free runtime image.
RUN ["/container/venv/bin/python", "-c", "import cairosvg; assert cairosvg.svg2png(bytestring=b'<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2\" height=\"2\"><rect width=\"1\" height=\"1\"/></svg>').startswith(b'\\x89PNG')"]

# Constraint: The runtime hardened image lacks a shell (/bin/sh). 
# We execute the healthcheck via python directly.
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD ["python", "/container/healthcheck.py"]

ENTRYPOINT ["/usr/bin/dumb-init", "--", "python", "/container/entrypoint.py"]