FROM python:3.12-slim WORKDIR /app # tzdata/fonts/Node.js all from Debian's own repo in one layer -- no # external curl/gnupg dance needed (see below for why that changed). # tzdata: python:3.12-slim doesn't include it by default, so the # zoneinfo database backing the web UI's "Timezone" setting (used by # "Quiet hours") would have no named zones to resolve without this -- # ZoneInfo() would raise for anything other than "UTC". # fontconfig/fonts-dejavu-core: whiteboard mode's render-service/ (own # README there) needs something to render whiteboard text with. # # Node.js: whiteboard frame mode's render-service/ runs as a second # process in this same container rather than a separate compose service # -- it's a lightweight, stateless, localhost-only sidecar with nothing # worth independently scaling or restarting. Used to be installed via # NodeSource's setup script (Debian's own nodejs package was too old for # jsdom's minimum back when this base image tracked Debian bookworm) -- # switched to Debian's own `nodejs`/`npm` packages after NodeSource's # deb.nodesource.com started intermittently 403ing on both its setup_*.x # scripts *and* its GPG key (a live NodeSource-side S3/CDN issue, # confirmed 2026-07-27 by hitting deb.nodesource.com directly -- some # setup_NN.x paths 403, others 200, no consistent pattern, so no # NodeSource-hosted install path could be trusted not to silently break # again). This base image now tracks Debian trixie, whose own `nodejs` # package is 20.19.2 -- inside jsdom 29's stated engines range # (`^20.19.0 || ^22.13.0 || >=24.0.0`) and well above express/resvg-js's # much lower floors -- so there's no longer a version gap to route # around NodeSource for. One less external dependency, and no more # curl-piped-into-bash (that pattern is also what let the NodeSource # failure go undetected here in the first place: `curl -f ... | bash -` # on a 403 hands bash an empty, "successful" script instead of failing # the RUN outright). RUN apt-get update && apt-get install -y --no-install-recommends \ tzdata fontconfig fonts-dejavu-core nodejs npm \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . # Split across several layers rather than one `pip install -r # requirements.txt` -- same Cloudflare single-blob/layer payload-size # limit as render-service's npm installs below. The single combined # layer was measured at ~113MB unpacked, over the limit on its own. # Isolating the three largest packages gets every layer's unpacked size # well clear of 100MB (sqlalchemy ~15MB, pillow ~19MB, pypdfium2 ~8MB, # the remaining `-r requirements.txt` layer ~71MB). Each package # version here still comes from requirements.txt (`pip install -r` for # everything that doesn't need its own layer skips these three, since # pip sees them already satisfied); the explicit versions below just # control *when* each installs -- same "single source of truth, just # splitting *when* it installs" tradeoff as the npm section's --no-save # comment below. RUN pip install --no-cache-dir sqlalchemy==2.0.51 RUN pip install --no-cache-dir pillow==12.3.0 RUN pip install --no-cache-dir pypdfium2==5.12.1 RUN pip install --no-cache-dir -r requirements.txt # render-service/'s dependencies installed as several separate layers # rather than one `npm install` covering all of them -- a from-scratch # push of this image once hit Cloudflare's payload-size limit on a # single blob/layer upload (the registry sits behind it), and splitting # a big layer into several smaller ones is the direct fix for exactly # that failure mode, independent of anything about the registry itself. # --no-save: package.json already fully declares these (with the exact # same version pins used here) as the single source of truth for what # this service depends on -- these calls are just about *when* each one # gets installed for layer-size reasons, not re-deciding what's needed. COPY render-service/package.json ./render-service/package.json WORKDIR /app/render-service RUN npm install --omit=dev --no-save express@^5.2.1 && npm cache clean --force RUN npm install --omit=dev --no-save jsdom@^29.1.1 && npm cache clean --force RUN npm install --omit=dev --no-save @excalidraw/utils@0.1.3-test32 && npm cache clean --force RUN npm install --omit=dev --no-save @resvg/resvg-js@2.6.2 && npm cache clean --force WORKDIR /app COPY render-service/server.js ./render-service/server.js COPY app ./app COPY start.sh . RUN chmod +x start.sh EXPOSE 8420 CMD ["./start.sh"]