-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathdockerfile
More file actions
99 lines (89 loc) · 5.58 KB
/
Copy pathdockerfile
File metadata and controls
99 lines (89 loc) · 5.58 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
# ── Builder stage ─────────────────────────────────────────────────────────────
# Compiles pycairo (and any future C-extension wheels) against the cairo dev
# headers, then we copy only the resulting wheels into the runtime image so the
# ~200MB of build toolchain doesn't ship to users.
FROM python:3.11-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libcairo2-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --wheel-dir /wheels --no-cache-dir -r requirements.txt
RUN find /wheels -type f -name 'opencv_python-*.whl' -delete
# ── Runtime stage ─────────────────────────────────────────────────────────────
FROM python:3.11-slim
WORKDIR /app
# libcairo2 (runtime only — no -dev headers needed) for pycairo/cairosvg;
# tini as PID 1 so orphaned processes get reaped (see CMD below).
# ElfHosted fork: no gosu — the container runs as a fixed non-root uid (see
# below); on Kubernetes the deployment SecurityContext (runAsNonRoot/fsGroup)
# owns user + volume-permission policy, so there's no root-startup drop dance.
RUN apt-get update && apt-get install -y --no-install-recommends \
libcairo2 \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /wheels /wheels
# RapidOCR's GUI OpenCV dependency is API-compatible with the headless wheel we
# intentionally install. Normalize its installed metadata so `pip check` and
# dependency scanners do not report the deliberate substitution as broken.
RUN pip install --no-cache-dir --no-deps /wheels/*.whl \
&& sed -i 's/^Requires-Dist: opencv_python/Requires-Dist: opencv-python-headless/' \
/usr/local/lib/python3.11/site-packages/rapidocr-*.dist-info/METADATA \
&& pip check \
&& rm -rf /wheels
# Bake the PP-OCRv5 Mobile detector into the image. TEXTLESS_TEXT_DETECTION is
# off by default in the ElfHosted fork, but baking costs only ~4.6MB of image
# and means opting in needs no runtime download: no stalled first low-vote
# textless request once enabled, and it survives cache-
# volume wipes / works on air-gapped hosts. Adds ~4.6MB to the image, and makes
# the build depend on PPOCR_MODEL_URL being reachable. Opt out for a lean image
# (e.g. if you disable detection) — the model then downloads once at runtime:
# docker build --build-arg BAKE_PPOCR_MODEL=false ...
# (or set BAKE_PPOCR_MODEL=false in .env when building via compose)
ARG BAKE_PPOCR_MODEL=true
ARG PPOCR_MODEL_URL=https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.8.0/onnx/PP-OCRv5/det/ch_PP-OCRv5_det_mobile.onnx
ARG PPOCR_MODEL_SHA256=4d97c44a20d30a81aad087d6a396b08f786c4635742afc391f6621f5c6ae78ae
RUN if [ "$BAKE_PPOCR_MODEL" = "true" ]; then \
apt-get update && apt-get install -y --no-install-recommends curl && \
mkdir -p /app/models && \
curl -fsSL "$PPOCR_MODEL_URL" -o /app/models/ch_PP-OCRv5_det_mobile.onnx && \
echo "$PPOCR_MODEL_SHA256 /app/models/ch_PP-OCRv5_det_mobile.onnx" | sha256sum -c - && \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* ; \
fi
# ElfHosted fork: explicit numeric uid/gid 568 so Kubernetes runAsNonRoot
# validation succeeds without resolving /etc/passwd, matching ElfHosted's
# per-app convention (helmrelease securityContext).
RUN groupadd --gid 568 appuser \
&& useradd --uid 568 --gid 568 --shell /bin/sh --create-home appuser
# Copy app files and create the cache + Prometheus multiproc dirs while still
# root, then hand ownership to 568. The cache dir is a runtime volume mount;
# on k8s fsGroup fixes its permissions, on compose chown the host dir before
# first `up` (see compose.yaml).
COPY . .
RUN mkdir -p /app/cache/tmdb_posters /app/cache/tmdb_logos /app/cache/composites \
/tmp/postersplus-prom \
&& chown -R 568:568 /app /tmp/postersplus-prom
# Numeric so kubelet can verify runAsNonRoot without /etc/passwd.
USER 568
# Exec form on purpose: the shell form ran `sh -c "python3 ... || exit 1"`, and
# when the probe overran its timeout Docker SIGKILLed only the sh it exec'd.
# The python3 child survived, was reparented to PID 1, and sat there as a
# root-owned `[python3] <defunct>` once it finished — uvicorn was PID 1 and
# never reaps children it didn't spawn. One zombie per overrun, which on a
# small VPS mid-render was every few probes. With no shell in between the
# probe process is the one Docker kills. A non-zero exit already marks the
# container unhealthy, so the `|| exit 1` bought nothing.
#
# The probe uses http.client rather than urllib.request: it imports a
# fraction as much, which matters when the CPU is busy compositing and the
# interpreter start-up is most of the probe's budget. The 10s timeout gives
# that start-up room on a loaded 1-vCPU host.
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD ["python3", "-c", "import http.client; c = http.client.HTTPConnection('localhost', 8000, timeout=8); c.request('GET', '/health'); r = c.getresponse(); raise SystemExit(0 if r.status == 200 else 1)"]
# tini as PID 1: reaps any orphan that lands on it and forwards signals, so a
# process Docker leaves behind (an exec'd shell's child, a killed probe) can
# never accumulate as a zombie. entrypoint.sh still execs into uvicorn — in
# this fork already as uid 568, with no gosu drop.
CMD ["/usr/bin/tini", "--", "/bin/sh", "entrypoint.sh"]