0. Series Loop (Follow along without public source code)
End-to-end pipeline: Vue frontend → api/routes/chat.py → Guide multi-turn SSE → run_analysis_pipeline (parse → analyze → match → report) → tools/pdf_exporter PDF.
This article: 15/17 · Deployment Ring · Docker
| Stage | User Visible | Code Entry | Article |
|---|---|---|---|
| Create session | Welcome message | POST /api/sessions | 09 |
| Multi-turn conversation | SSE streaming | chat/stream → run_guide_single_turn | 06, 14 |
| Information sufficient | Start analysis | _run_analysis_background | 05, 07 |
| Resume parsing | Progress 30% | run_resume_parser | 12 |
| Profile/RIASEC | Progress 50% | run_profile_analyzer | 03, 13 |
| Career matching | Progress 70% | run_career_matcher | 02 |
| Report | Progress 90% | run_reporter | 11 |
| Download PDF | File | GET …/report/pdf | 11, 15 |
| Description | |
|---|---|
| Before reading this | Article 11 (fonts), Article 17 (env) |
| After reading this | Understand how frontend and backend are packed into the same image based on Dockerfile stage descriptions |
| Next ring | Article 16: Entry middleware (Article 16) |
Full series loop index: SERIES-LOOP.md
1. What Problem to Solve
iCan is not a pure API service: the Vue frontend must be built into a static directory, the backend FastAPI + LangGraph needs to connect to MySQL/Chroma, Embedding relies on sentence-transformers (which indirectly pulls PyTorch), and Reporter also uses ReportLab/matplotlib to generate Chinese PDFs.
In the macOS development environment, “everything works fine”, but common issues when moving to a Linux container include:
- pip automatically installs the CUDA version of PyTorch, causing the image size to explode and unnecessary downloads on GPU-less servers;
- PDF/radar chart Chinese characters become squares (container lacks CJK fonts);
COPY frontend/brings the host’snode_modulesinto the image, causing Vite build anomalies;- The Debian new version’s GTK/Pango package name changes causing
apt-get installto fail.
The project root’s Dockerfile + docker-compose.yml records the actual solutions to these problems.
2. Implementation Location
| File | Responsibility |
|---|---|
Dockerfile |
Two stages: Node builds frontend → Python runtime image |
docker-compose.yml |
Ports, environment variable defaults, model and Chroma volume mounts |
frontend/vite.config.js |
outDir: '../static', base: '/static/' |
tools/pdf_exporter.py |
ReportLab / matplotlib Chinese font detection chain |
config.py |
Fields corresponding to container environment variables like EMBEDDING_MODEL_PATH, CHROMA_PERSIST_DIR |
3. Multi-stage Dockerfile Structure
Stage 1: Frontend Build (node:18-alpine)
1 | |
frontend/vite.config.js outputs the build artifacts to the static/ directory in the repository root (inside container: /build/../static → /static/):
1 | |
FastAPI mounts static resources from ./static/; base: '/static/' ensures the packaged JS/CSS paths align with the backend routes.
Stage 2: Python Runtime (python:3.10-slim)
1 | |
Note that the final WORKDIR is /app/src, so uvicorn ican.main:app uses src as the Python path root, consistent with local cd src && uvicorn.
4. docker-compose and Environment Overrides
docker-compose.yml does not hardcode DeepSeek; it defaults to the host’s Ollama:
1 | |
The Embedding model is mounted via a read-only volume: /ican/iCan/llm_models/bge-m3:/app/models/bge-m3:ro. Chroma data is persisted with a named volume chroma-data. This corresponds one-to-one with the EMBEDDING_MODEL_PATH and CHROMA_PERSIST_DIR fields in config.py (see Article 17).
5. Pitfall 1: PyTorch CUDA Version Gets Indirectly Installed
Phenomenon
requirements.txt contains dependencies like sentence-transformers. If you directly run pip install -r requirements.txt, pip may pull the CUDA version of torch (700MB+), which is entirely unnecessary on CPU servers.
Project Solution
In the Dockerfile, first install the CPU version of torch, then install the remaining dependencies:
1 | |
The pre-installed torch satisfies sentence-transformers‘s dependency, avoiding redundant GPU builds. matplotlib is installed in a separate line to ensure all PDF chart dependencies are complete in the slim image.
6. Pitfall 2: Chinese PDF/Chart Garbled in Container
Phenomenon
ReportLab’s default Helvetica does not contain Chinese characters; matplotlib radar charts and bar chart labels appear as tofu squares.
Image Layer
The Dockerfile installs fonts-noto-cjk and the necessary Pango/Cairo stack for PDF rendering:
1 | |
Code Layer
In tools/pdf_exporter.py, when registering the ReportLab font in _build_pdf, it probes paths in order, with Linux Docker paths listed first:
1 | |
For matplotlib (_generate_radar_chart, _generate_bar_chart), set:
1 | |
The first font in the list, "Noto Sans CJK SC", matches the Noto package name installed via apt. Just installing fonts without modifying the code might work on macOS but still fall back to Helvetica in Docker—both image and code layers must be addressed.
7. Pitfall 3: COPY frontend Overwrites node_modules
Phenomenon
First npm install, then COPY frontend/ ./ copies the developer’s local (possibly macOS) node_modules into the image, overwriting the freshly installed Linux dependencies, causing Vite/esbuild permission or platform binary mismatches.
Project Solution
1 | |
An equivalent approach is to exclude frontend/node_modules in .dockerignore to prevent COPY from bringing it in. The current Dockerfile chooses to reinstall after COPY, which is explicit and reproducible.
8. Pitfall 4: Debian Package Name Changes
Phenomenon
Old documentation often references libgdk-pixbuf2.0-0, but on newer Debian (e.g., trixie-based slim base images), this package is missing.
Project Solution
Use the new package name libgdk-pixbuf-2.0-0 (see line 20 of Dockerfile). WeasyPrint/ReportLab indirectly depend on GDK-Pixbuf; if this package is missing, PDF generation may fail during import or rendering, and the error message may not directly point to the package name—worth noting separately.
9. Pitfall 5: Compose Default DSN and Missing MySQL Service
In docker-compose.yml, the DB_URL default points to mysql:3306, but the compose file only defines the app service, no mysql container. Without additional orchestration, the database connection will fail upon container startup—you need to either add a MySQL service, change DB_URL to a host-reachable DSN, or rely on SQLite (the default in config.py) during development.
Similarly, the Embedding volume source path is hardcoded as /ican/iCan/llm_models/bge-m3. Before deploying on a different machine, this must be changed to the actual host path; otherwise, EMBEDDING_MODEL_PATH points to an empty directory, and vector retrieval will fail at runtime.
10. Accelerating Builds in China
In the same Dockerfile, three mirror sources are used:
| Layer | Approach |
|---|---|
| apt | sed replace deb.debian.org → mirrors.aliyun.com (handles both debian.sources and sources.list) |
| pip | -i https://pypi.tuna.tsinghua.edu.cn/simple |
| npm | --registry=https://registry.npmmirror.com |
PyTorch CPU still uses the official --index-url https://download.pytorch.org/whl/cpu, combined with the Tsinghua mirror.
11. Runtime Directory and Data Volumes
Pre-create directories inside the image:
1 | |
The compose file binds ./logs to /app/logs and uses a named volume for Chroma. Embedding weights are not baked into the image (large size); they are mounted via host path—before deployment, ensure EMBEDDING_MODEL_PATH matches the volume source path.
12. Summary
- Two-stage build: Alpine Node compiles frontend → slim Python runs API, static files via
COPY --from=frontend-builder /static/. - CPU torch first, then requirements, controlling image size and satisfying the Embedding stack.
fonts-noto-cjk+tools/pdf_exporter.pyfont chain for stable Chinese in PDF and matplotlib.- Reinstall node_modules after COPY frontend, avoiding cross-platform node_modules contamination.
- Use
libgdk-pixbuf-2.0-0as package name; compose overrides LLM/DB via environment variables, defaulting to Ollama instead of hardcoding a cloud model. - Check MySQL and Embedding mount paths; compose defaults won’t automatically spin up a database or download models.
Next article: FastAPI middleware and rate limiting (Article 16).
Appendix: Key Source Code (Line-by-Line Comments)
The following code is excerpted from the iCan implementation. Each line has a Chinese comment above it, allowing you to follow along even without the public repository.
Generated by: python3 bin/build-ican-annotated-snippets.py
Dockerfile Multi-stage (Excerpt)
1 | |
PDF Font Registration
1 | |
Series Navigation
| Article | Topic |
|---|---|
| 1 | System Overview |
| 2 | Five Agent Collaboration |
| 3 | Holland RIASEC |
| 4–7 | State · Routing · Nesting · Fault Tolerance |
| 8–11 | LLM Layer · SSE/WS · DB Migration · PDF |
| 12–14 | JSON Prompt · RIASEC Prompt · Guide Prompt |
| 15–17 | Docker · Middleware · Configuration |