-
-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathDockerfile
More file actions
210 lines (180 loc) · 10.6 KB
/
Copy pathDockerfile
File metadata and controls
210 lines (180 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# 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
RUN 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:686404e54011e51bd2f4eb050e28ca9d560703db286c8cb6cf8efaaa0bf384bc 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
# - 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; \
\
# 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 \
ghostscript \
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.
EXTRACT_DEPS_TARGET=/dpkg-export sh /tmp/extract_deps.sh /usr/bin/gs && \
\
# 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/
from backend.image_converter.config import settings
from rembg import new_session
model_name = settings.get().rembg.model_name
new_session(model_name)
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 3: FINAL RUNTIME
# ------------------------------------------------------------------------------------------
FROM dhi.io/debian-base:trixie-debian13@sha256:f5cabbc76f75c55151aa0f23d06d643cbb04a07de4c8bf88f003ca7dcbae8527 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), and background removal 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
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=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
# 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"]