April 22, 2026, 12:52 p.m.
This Website
Audio
This Website
*AI-generated content, based on my own information and instructions.
This website was born in 2020, when all I wanted was to learn how to publish something — anything — online. It looks quite different today: it went from a ready-made site builder to a custom Django application, and from a single AWS VM to a Kubernetes cluster running locally, with automated deployment and resilience layers that didn't exist in the first version.
What you'll find here
- Technical Architecture — how the site works today: network infrastructure, Kubernetes, CI/CD, and the stack choices behind it.
- Repository Structure — the source code's directory tree, fetched live.
- Original version (2021) — the motivation-and-learning account I wrote when I first built the site, preserved exactly as it was published back then — images and beginner's voice included.
As a data analyst, most of my work could always be done locally and offline — scraping data, running regressions, training models. But the world kept moving toward the cloud, and I was missing a piece: knowing how to publish and automate all of that without leaving my personal computer running. This site was how I chose to fill that gap — and it's still where I experiment with new technologies and share projects and studies.
Below is the current architecture; at the end of the post, the original 2021 text tells the story of how it all started.
Technical Architecture
This document describes, at a high level, how caiodonalisio.com works today — from network infrastructure to the CI/CD pipeline.
Overview
1. Network Infrastructure
The site runs on a single-node Kubernetes cluster, hosted on a local mini-PC:
inter-VLAN routing and firewall
VLAN isolation — the server sits on a dedicated VLAN, isolated from the rest of the home network. The router ensures the server VLAN has no access to the home network, that only specific devices can reach management SSH, and that outbound internet access is limited to what the Cloudflare Tunnel needs. This segmentation means that even if the server were compromised, there would be no lateral access to home devices.
Internet exposure — there is no port forwarding on the router. All inbound traffic goes through the Cloudflare Tunnel, which keeps a persistent outbound connection to Cloudflare's edge. That removes the need for a static public IP, open firewall ports, or manual TLS certificate management.
2. Kubernetes (K3s)
Single-node K3s with the following resources in the caiodonalisio namespace:
| Resource | Replicas | Role |
|---|---|---|
| Django / Gunicorn | 2 | Web application |
| NGINX | 1 | Reverse proxy + static files |
| PostgreSQL | 1 | Database |
| Redis | 1 | Cache + session store |
| Cloudflared | 1 | Tunnel daemon |
Storage — provisioned via K3s's local-path: staticfiles-pvc (2Gi) for collected static files, media-pvc (2Gi) for uploads and media, postgres-pvc (10Gi) for PostgreSQL data.
Health checks and resilience — the Django deployment exposes a /health/ endpoint, checked by readiness and liveness probes:
readinessProbe → /health/ (60s delay, every 15s, 3 failures)
livenessProbe → /health/ (90s delay, every 30s, 5 failures)
Combined with a PodDisruptionBudget (minimum 1 replica available) and zero-downtime rolling updates (maxUnavailable: 0, maxSurge: 1), this guarantees no downtime during deploys. A pre-stop hook waits 15 seconds before SIGTERM, letting in-flight connections drain.
3. Cloudflare Tunnel
The cloudflared daemon runs as a deployment in the cluster, routing public hostnames to the internal NGINX service. Three configuration details matter in practice:
hostNetwork: true— avoids intermittent 502s on single-node clustersdnsPolicy: ClusterFirstWithHostNet— keeps internal service resolution workingprotocol: http2— multiplexed connection to Cloudflare's edge
4. Application Stack
| Component | Version | Notes |
|---|---|---|
| Python | 3.12 | — |
| Django | 5.1 | Modular settings (dev / prod / test) |
| Gunicorn | — | 4 workers, port 3334 |
| Celery | 5.4 | Configured, workers disabled |
| PostgreSQL | 16.4 | Primary persistence |
| Redis | 7.4.1 | Sessions, cache, Celery broker |
Simplified Django project structure:
website/ # Project settings
├── settings/ # base / production / development
├── urls.py # Routes + /health/ endpoint
└── celery.py
blog/ # Main app
├── models.py # Post, PostFile, PostBook
├── views.py # Markdown rendering
└── storage.py # Custom OverwriteStorage
accounts/ # Authentication (CustomUser)
pages/ # Static pages
Serving — NGINX acts as reverse proxy and serves /static/ and /media/ directly; WhiteNoise is the fallback for static files; Gunicorn handles dynamic requests over WSGI.
5. CI/CD
The GitHub Actions workflow triggers on every push to main. Versions are generated automatically by python-semantic-release from the commit message: fix: bumps PATCH, feat: bumps MINOR, BREAKING CHANGE: bumps MAJOR.
Argo CD watches the repository's k8s/ directory and applies changes automatically (prune and self-heal enabled, with progressive retries). The pipeline itself updates the image tag in the manifest, which triggers the sync.
6. Containerization
Multi-stage build, using uv (a Rust-based package manager) to speed up dependency resolution:
# Stage 1: builder — resolves and installs dependencies with uv
FROM python:3.12-slim AS builder
...
# Stage 2: runtime — copies only what's needed
FROM python:3.12-slim
COPY --from=builder /.venv /.venv
COPY . /usr/src/
EXPOSE 3334
CMD ["./entrypoint.sh"]
entrypoint.sh runs on container startup: waits for PostgreSQL to be reachable, runs collectstatic and migrate, syncs media files, and finally starts Gunicorn.
7. Full Request Flow
8. Security Considerations
| Layer | Measure |
|---|---|
| Network | VLAN isolation, no port forwarding |
| Edge | Cloudflare DDoS protection and WAF |
| Transport | TLS managed by Cloudflare |
| Cluster | Network policies, secrets for credentials |
| Application | CSRF, secure cookies, ALLOWED_HOSTS |
| CI/CD | Image vulnerability scanning (Trivy) |
Last updated: August 2026.
Repository Structure
Below is the directory tree of this site's repository, fetched and rendered live.