Epic News Docker Deployment Guide¶
This guide covers building and running the Epic News containers, plus the problems that have actually bitten us.
The Dockerfile is the source of truth¶
There is one Dockerfile at the repository root. Read it — it is heavily
commented, and every comment records a failure that was paid for once already.
This guide does not reproduce it, because a copied example goes stale and then
teaches the opposite of what the real file does.
It is multi-stage. A builder stage resolves and installs the venv with uv
and is never shipped. A runtime-base stage starts from a plain
python:3.13-slim-bookworm, copies the finished venv and the source out of the
builder, and creates the writable data directories. Two targets build on it:
| Target | Serves | Port(s) | Use it for |
|---|---|---|---|
api |
FastAPI via uvicorn |
8000 | The API on its own; what most deployments want. |
streamlit |
The Streamlit UI | 8501 | The UI; it runs the flow in its own process. |
Building¶
docker build --target api -t epic_news-api:latest .
docker build --target streamlit -t epic_news-streamlit:latest .
Or through the Makefile, which is the same commands with the project's tags:
Running¶
Or with docker-compose.yml, which adds the bind mounts for db/, data/,
output/ and .env:
docker compose up -d # api + streamlit from the published GHCR images
docker compose up -d --build # build both from the Dockerfile targets instead
docker compose up -d api # the API alone
Properties worth not breaking¶
These are deliberate, and each was a bug before it was a rule:
- The container runs as
myuser, and/app,/app/srcand/app/.venvstay root-owned. The COPY lines carry no--chown;--chownis recursive and would hand the runtime user write access to its own code and interpreter. Only the seven data directories (db data output traces checkpoints debug logs) are chowned. New write targets get added to that list, not fixed by widening ownership. - The venv and the source ship as one 1.52 GB layer, and splitting them does
not help. It was measured. The project is an editable install, so
uv syncreinstalls it whenever the source changes and rewrites its install metadata inside the venv —uv_cache.json, which records an mtime for the source tree, andRECORD, which hashes it. That is enough forCOPY --from=builder /app/.venvto miss cache after any source edit. Anyone attacking the rebuild cost needs a separate deps-only venv stage; a COPY split alone buys nothing. - Healthchecks call
python -c urllib.request, notcurl.curlis not in the slim base; a healthcheck that shells out to a binary the image does not contain reports unhealthy forever. Nothing invokesuv runat runtime either —uvis deleted from the image. - No
VOLUMEdeclarations.VOLUMEmakes Docker create a root-owned anonymous volume on every run, unwritable bymyuserand leaked to disk. Compose bind-mounts host paths over those directories instead. - The build context is an allowlist.
.dockerignoreexcludes*and re-includes only what the build needs. A denylist once let a 234 MB.mypy_cacheinto every published image.
Troubleshooting and Lessons Learned¶
Python Import Errors¶
ModuleNotFoundError: No module named 'epic_news': This error occurs when theuvicorncommand can't find the application.epic_newsresolves because the builder's seconduv sync --locked --no-devinstalls the project into the venv, which records/app/srcin an editable.pthfile. The application target is thereforeepic_news.api:app, and/app/srcmust be copied to that exact path — a venv copied without its matching source tree imports nothing.ModuleNotFoundError: No module named 'src': This error was caused by incorrect absolute imports within the application code (e.g.,from src.epic_news...). The fix was to remove thesrc.prefix from all imports, asepic_newsis the top-level package inside the container, notsrc.
Pandoc for DOCX Reports¶
- DOCX reports fail with "No pandoc was found":
pypandocdrives the systempandocbinary, which the runtime stage installs withapt-get(ADR-013). Keeppandocin that package list. PDF export (WeasyPrint) was removed, so its Pango libraries are no longer needed.
Application Hangs at Startup¶
- Symptom: The container starts, but the logs stop at a certain point and
uvicornnever reports that it's running. In our case, the last log message wasActions cache is outdated, refreshing cache.... - Cause: A third-party library (
composio_crewai) was performing a long-running, blocking operation as soon as it was imported. Because it was imported at the top level of a module, it blocked the entire application startup process. -
Solution (Lazy Loading): The import was moved from the top of the file into the specific method where the library was actually used. This defers the expensive import until it's needed, allowing the server to start without delay.
Before (Problem):
from composio_crewai import ComposioToolSet # <-- This blocks at startup @CrewBase class CompanyNewsCrew: def __init__(self): self.toolset = ComposioToolSet() # ...After (Solution):
Common Runtime Issues¶
- Database Errors
- Check file permissions on the db directory
- Missing Data Files
- Verify that the data directory contains the required feedly.opml file
- Environment Variables
- Confirm all required environment variables are set in the .env file
Logs¶
To view container logs:
Maintenance¶
Updating Images¶
Pull the latest images:
Restart services with new images:
Backup¶
Backup important data regularly: