Files
espresso_frame/server/Dockerfile
T
tfaour 8ea1c53ec3
Build and push server image / test (push) Successful in 39s
Build and push server image / build-and-push (push) Failing after 2m34s
Build and push server image / deploy (push) Has been skipped
Add experimental HTML/CSS "modern" render style for weather widget
The weather widget's icons/layout are hand-drawn PIL primitives -- clean
under quantization but flat, no gradients/shadows. Adds an opt-in
render_style="modern" (current/daily modes only) that instead renders a
Jinja2 template through a persistent headless-Chromium browser
(app/html_render.py), following the approach of Tesserae, an open-source
e-ink dashboard targeting this same panel family.

Key design points:
- The Chromium dependency (Playwright) is lazily imported only when a
  weather widget actually uses "modern" style, and the background browser
  itself only launches on first use -- every other widget type, and this
  one's own classic/hourly/multi_city paths, never pay for it.
- No Frame-level dithering setting needed: html_render dithers its own
  rendered widget to exact palette colors (Bayer/ordered, not
  Floyd-Steinberg) before compositing, so the shared whole-canvas
  Floyd-Steinberg pass sees zero quantization error there and leaves it
  untouched -- same trick draw_text/hand-drawn icons already use. Floyd-
  Steinberg keeps working unchanged for photos and every other widget.
- A "Load calibrated Spectra 6 preset" button in Advanced configuration
  offers a community-measured palette (data ported from
  paperlesspaper/epdoptimize, Apache 2.0) as an alternative starting
  point to the existing idealized DEFAULT_PALETTE_RGB -- fills the
  existing palette table, doesn't save by itself.

Known open risk, not resolved here: a headless Chromium binary is far
larger than the ~100MB single-layer limit that already forced this
project's pip/npm installs into split layers, and (unlike those) is a
single ~180MB file that can't be split across layers by ordinary
Dockerfile restructuring. Flagged prominently in server/Dockerfile and
docs/widgets.md -- treat this render style as experimental/local-only
until that's resolved.
2026-07-30 22:18:43 +00:00

121 lines
6.7 KiB
Docker

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/*
# EXPERIMENTAL, likely unmergeable as-is -- see below. System libs a
# headless Chromium needs (app/html_render.py, the weather widget's
# opt-in "modern" render style), trimmed from Playwright's own full
# `install-deps chromium` list to just what a headless (no Xvfb),
# Latin-text-plus-emoji use case needs: dropped xvfb (only needed for a
# *headed* browser) and the CJK/Cyrillic/Thai locale font packages
# (fonts-ipafont-gothic, fonts-wqy-zenhei, fonts-tlwg-loma-otf,
# xfonts-cyrillic, xfonts-scalable, fonts-freefont-ttf, fonts-unifont) --
# fonts-noto-color-emoji is the one that actually matters here (real
# color emoji in the weather icons, vs. WeasyPrint/Pango's monochrome
# fallback glyphs in this feature's original spike).
RUN apt-get update && apt-get install -y --no-install-recommends \
libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 \
libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 \
libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 \
libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \
fonts-noto-color-emoji libfontconfig1 libfreetype6 fonts-liberation \
&& 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 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, 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 playwright==1.61.0
RUN pip install --no-cache-dir -r requirements.txt
# KNOWN LIKELY BLOCKER, not resolved by pulling this into its own layer:
# `playwright install chromium-headless-shell` unpacks to ~262MB, and its
# single `chrome-headless-shell` binary alone (measured: 181MB) is one
# file -- unlike the pip/npm splits above (independently-installable
# smaller packages moved into their own layers), a single 181MB file
# can't be divided across multiple <100MB Docker layers by any ordinary
# COPY/RUN restructuring; the whole file lands in whichever layer's diff
# contains it. This almost certainly exceeds the same Cloudflare single-
# blob/layer limit that forced the pip/npm splits elsewhere in this file
# (see their comments) -- an actual push to this project's registry
# hasn't been attempted (would require pushing to `main`, which triggers
# deploy) to confirm, but there is no reason to expect a single 181MB
# blob to fit where combined ~113MB of many small wheels didn't. Needs a
# real resolution (a registry without this limit, hosting the browser
# binary outside the image, etc.) before this branch can actually ship --
# tracked as open, not silently assumed away.
RUN playwright install chromium-headless-shell
# 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/[email protected] && npm cache clean --force
RUN npm install --omit=dev --no-save @resvg/[email protected] && 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"]