# news-digest — base image for two containers in this stack: # # news-digest-worker — runs alpine's busybox crond + the one-shot # digest.py per fire (default ENTRYPOINT). # news-digest-web — runs uvicorn web:app (overridden in compose) # to serve /output as static + the tiny # hidden-items API at /api/*. # # Single image, two roles selected via compose `command:`. # Bind-mounted /output is the shared canvas: worker writes HTML, web # serves it. FROM python:3.12-alpine ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 # tzdata so $TZ works for cron + datetime; tini so signals propagate cleanly. RUN apk add --no-cache tzdata tini bash curl # fastapi + uvicorn[standard] for the web container; requests + jinja2 # for the worker. Both shipped in both containers — neither set is # heavy enough to justify splitting the image. # trafilatura: main-content extractor for the article-summary upgrade # (worker only — it pulls lxml + a handful of HTML utils, ~80 MB total). # libxml2-dev/libxslt-dev are for lxml's musl wheels. apk caches are # cleaned in the same RUN to keep the layer small. RUN apk add --no-cache --virtual .build-deps gcc musl-dev libxml2-dev libxslt-dev \ && apk add --no-cache libxml2 libxslt \ && pip install --no-cache-dir \ requests jinja2 trafilatura \ 'fastapi>=0.115' 'uvicorn[standard]>=0.30' \ && apk del .build-deps WORKDIR /app COPY digest.py /app/digest.py COPY seed-headlines.py /app/seed-headlines.py COPY web.py /app/web.py COPY templates /app/templates COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY run-digest.sh /usr/local/bin/run-digest.sh COPY crontab /etc/crontabs/root RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-digest.sh # Sentinel + first-run output dir VOLUME /output # Default ENTRYPOINT runs the worker (cron). The web container in # compose overrides both entrypoint and command to launch uvicorn. ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"]