How I Built CVFactory: Deploying an AI Cover-Letter Generator with Django & Docker
Introduction
In this post, I’ll walk you through my recent side project CVFactory—a web service that accepts a job posting and your career history, then leverages AI to generate a tailored cover-letter draft.
We’ll cover the motivation behind the idea as well as the technical and operational hurdles I encountered while turning a small prototype into a production-ready service.
1. Motivation — Why build an AI cover-letter generator?
Every developer has at least one “pet idea” they want to build someday. For me, that idea was simplifying cover-letter writing.
- Automating repetition — I wrote countless cover letters when job-hunting and noticed how repetitive the process is. I wondered, could tech remove this tedium?
- Exploring new tech — Large Language Models (LLMs) are booming. I wanted a hands-on project that used them in a practical domain like hiring.
- Full-stack practice — I aimed to own the whole journey: frontend, backend, DB, deployment. It was a good excuse to play with Django, Docker, and Northflank.
2. Tech stack & architecture — How did I build it?
- Backend: Django (auth & routing) + internal FastAPI microservice (LLM inference)
- Frontend: Vanilla HTML / CSS / JavaScript
- DB: SQLite (dev), PostgreSQL (prod)
- Deployment: Docker + Northflank + Cloudflare
A user fills a form on the frontend → Django calls an internal FastAPI microservice → the service hits the AI provider → returns a draft → Django sends it back to the browser. (I’ll dive into the FastAPI bits in a follow-up post.) Docker images are built and auto-deployed on Northflank.
3. Bumps on the road — What went wrong?
Static files war in Django
- Issue: CSS & JS worked in
DEBUG=True
but not inDEBUG=False
. - Fix: Learned about
STATIC_ROOT
,collectstatic
, and added Whitenoise.
# config/settings.py (snippet)
STATIC_URL = '/static/'
STATIC_ROOT = '/app/staticfiles/'
STATICFILES_DIRS = [
BASE_DIR,
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Bloated Docker images
- Issue: First image weighed hundreds of MB.
- Fix: Added
.dockerignore
, switched topython:3.8-slim-buster
, used multi-stage builds.
# Dockerfile (snippet)
FROM python:3.11-slim AS builder
WORKDIR /app
COPY . /app
# install deps fast using uv
RUN apt-get update && apt-get install -y curl ; \
curl -LsSf https://astral.sh/uv/install.sh | sh ; \
uv pip install --no-cache-dir --system -r requirements.txt
A new PaaS: Northflank
- Issue: Render’s free tier only supported cold starts, so I needed a free PaaS that provided always-on hot starts.
- Fix: Switched to Northflank—one of the very few free PaaS providers that still give you always-on hot starts—plus tight GitHub & Docker integration. Created
northflank.json
, and apurge_cloudflare_cache.py
script to clear CDN cache on deploy.
# purge_cloudflare_cache.py (snippet)
import os, requests, logging
logging.basicConfig(level=logging.INFO)
def purge_cache():
api_token = os.environ.get("CLOUDFLARE_API_TOKEN")
zone_id = os.environ.get("CLOUDFLARE_ZONE_ID")
requests.post(
f"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache",
headers={"Authorization": f"Bearer {api_token}"},
json={"purge_everything": True},
timeout=30,
)
4. Closing thoughts
CVFactory may be small, but building it taught me a ton—from Dockerizing Django to wiring an automated deploy pipeline.
If you have an idea stuck in your head, start small and ship it. The inevitable “debugging adventures” will level up your skills more than any tutorial. Happy coding!