Introduction to csabakeller.com
The demo is the site itself
Most portfolios tell you what someone can build. This one runs on it. csabakeller.com is a self-hosted Django site. A public landing page plus a markdown-driven technical repository with first-party page-visit analytics.
It is a working, single-author demonstration of how I design and operate production systems: a current stack, trade-offs I'm happy to defend in a review, and an operational footprint small enough for one person to run calmly.
I wanted to build something genuinely useful, a place to publish and be found. A place where I can share ideas, documentation, study notes and more, on a modern stack that a solo maintainer can run without a platform team behind them, making every decision one that would hold up under scrutiny. What follows is the shape of that system.
Tech Stack
The stack is current as of 2026 and chosen for longevity over novelty.
| Layer | Choice |
|---|---|
| Language | Python 3.14 |
| Framework | Django 6.0 |
| Database | PostgreSQL 18 (production), SQLite (development) |
| Web server | gunicorn behind nginx |
| Background work | Django Tasks framework, database-backed |
| Scheduling | systemd timers |
| API | Django REST Framework |
| Configuration | django-environ |
| Markdown pipeline | Markdown rendering, nh3 sanitiser |
| Error monitoring | Sentry |
This started from something I do every day. I write constantly — technical documentation and design write-ups at work, study notes and pet projects at home. Nearly all of it in markdown. What I wanted was one place where that raw markdown becomes clean and styled public HTML I can share with anyone, or for me to revisit later.
Python is what turns that into a single, coherent system rather than a bundle of glued-together tools. The same language that renders and serves the writing also drives the analytics, the background jobs, the pipelines and the webserver where everything becomes real. Everything under one roof, with room to grow into whatever I want to build next.
Architecture Principles & Design Drivers
The overriding driver is solo operability: a single maintainer being able to run this for years without it becoming a burden. Beneath that, a handful of principles shape the code.
Query and derived-data logic lives in the model layer — fat models, skinny views — with chainable query methods wrapped in a custom manager so that filtering compiles down to single, optimised SQL rather than leaking into view code.
Views themselves are class-based and compose through mixins, so shared behaviour (page titles, hit counts, performance logging) is written once and reused. Anything a reader shouldn't have to wait for is treated as a side effect and handled asynchronously — analytics being the obvious case. And the codebase is split one app per responsibility, so each concern stays bounded and independently testable.
Security is a default rather than an afterthought: the admin path is environment-driven rather than the guessable /admin/, brute-force lockout guards it in production, rendered markdown is sanitised on save, and visitor IPs are masked at write time to keep the analytics privacy-conscious.
Environments & Configuration
The system runs in two environments — a personal development machine and a Linode production server — that differ in database, host settings, secrets, and process supervision. Rather than duplicate configuration, a single settings package splits into a shared base plus dev, test, and prod layers that import it.
config/settings/
├── base.py # shared: apps, middleware, templates, TASKS backend, DRF, logging config
├── dev.py # imports base; DEBUG=True, local DB, console email, debug toolbar
├── test.py # imports base; MD5 password hasher, LOG_VIEW_PERFORMANCE on — used by pytest
└── prod.py # imports base; security flags, prod DB, Sentry init, prod hosts
This keeps environments cleanly separated and tidy. It's easy to develop and test new features before pushing them to production, which keeps the solo-developer workflow manageable and maintainable as the application grows.
Deployment & Operations
Production is intentionally boring, which in operations is the highest compliment. gunicorn serves the application over a Unix socket with nginx in front for TLS termination and static files. The background worker runs as its own service, and both are supervised by systemd, so they restart cleanly on their own.
Recurring work — nightly database backups, scheduled analytics roll-ups — runs as systemd timers rather than a separate scheduler daemon, again keeping the process count down.
Static files are served with content-hashed filenames, so a changed stylesheet ships under a new URL and long-lived browser caches never serve a stale asset after a deploy — a small detail that quietly prevents a common and maddening class of "it works on my machine" bug.
TLS certificates renew automatically. Deployment itself is a single scripted sequence — pull, migrate, collect static files, restart services — with each release tagged in Sentry by its git commit, so any error in production is traceable to the exact code that caused it. Backups run nightly and are pushed off-box to object storage, because a backup that lives only on the server it's protecting isn't really a backup.
Non-Functional Requirements
| Quality | How it's met |
|---|---|
| Performance | Async analytics writes keep them off the reader's path; related-object queries are fetched in bulk; static assets are content-hashed and long-cached |
| Security | Environment-driven admin path, production brute-force lockout, sanitised markdown, HTTPS-only |
| Privacy | Visitor IPs are masked at write time (/24 for IPv4, /48 for IPv6); monitoring is configured to drop personal data |
| Maintainability | One shared settings package, a single change-control channel, and full line coverage across both applications |
| Resilience | Single server by design — recovery is restore-from-backup, with nightly off-box dumps plus provider snapshots underneath |
| Testing | pytest with pytest-django and factory_boy data factories; the suite runs fully in-memory and rolls each test back in its own transaction, so it never touches real data |
The resilience row is stated plainly on purpose. This is a content site for one author, not a system that needs to survive a data-centre outage without blinking, so I've spent the effort on durable backups rather than on redundancy the workload doesn't warrant. Matching the engineering to the problem is itself a design decision.
Purpose and aim
For anyone evaluating me — a hiring manager, a recruiter, a prospective client — the pay-off is that this is evidence rather than assertion. It's a live system on a genuinely current stack, carrying the operational discipline that separates a weekend demo from something you'd actually trust in production: monitoring, tested code, hardened entry points, real backups.
It runs at negligible cost on a single small server, and because the configuration is split cleanly and the environment is reproducible, it can be rebuilt from scratch rather than nursed. That combination — modern, defensible, and cheap to operate — is exactly what I aim to bring to other people's systems.
In closing
This site is the short version of how I think about building things and keep: current tools, honest trade-offs, and an operational footprint sized to the problem. If that's the kind of thinking you want on your own systems, I'd be glad to talk — the contact page is the fastest way to reach me, and there's more in the repository if you'd like to see the detail behind any of the above.