Building a secure kindergarten website with Django
TL;DR
- Built a Django-based website for a kindergarten with Bootstrap template
- Avoided WordPress for simplicity and focused on static content
- Implemented secure file sharing for educators and parents
- Used nginx X-Accel-Redirect for efficient authenticated media delivery
- Hosted on Hetzner Cloud for cost-effective traffic management
When my children's kindergarten needed a new website, I decided to build something custom rather than using a typical CMS solution. The result is kinderhausdonbosco.de, a Django-based website that prioritizes simplicity and security.

Why not WordPress?
The decision to avoid WordPress was deliberate, based on three key considerations:
-
Simplicity focus: The website primarily serves static content - basic information about the kindergarten, contact details, and educational philosophy. A full CMS would be overkill.
-
Dynamic content strategy: Instead of managing dynamic content on the website, we leverage Instagram for announcements and updates. This approach is more cost-effective, reaches parents more effectively, and keeps them engaged through a platform they already use daily.
-
Secure file sharing: The most important feature is secure distribution of photos and videos between educators and parents through a protected download system.

The technical challenge: Secure media delivery
The most interesting technical aspect was implementing secure file downloads. Simply serving media files through Django is inefficient, especially for larger files. Typically, you'd use nginx to serve static files directly, but our files needed authentication.
Here's the elegant solution using nginx's X-Accel-Redirect feature:
# upload/views.py
@login_required
def download_file(request, filename: str):
file_upload = get_object_or_404(FileUpload, name=filename)
# Track download count
FileUpload.objects.filter(id=file_upload.id).update(
download_count=F('download_count') + 1
)
# Create pretty filename
base_filename, extension = os.path.splitext(file_upload.file_name)
extension = extension[1:]
slug = slugify(file_upload.name)[:100]
pretty_name = f"{slug}.{extension}"
# Set proper content type
content_type, _ = mimetypes.guess_type(file_upload.file_name)
if content_type is None:
content_type = 'application/octet-stream'
response = HttpResponse(content_type=content_type)
response["Content-Disposition"] = f"attachment; filename={pretty_name}"
response["X-Accel-Redirect"] = f"/media/protected/{file_upload.file_name}"
return response
The corresponding nginx configuration handles the internal redirect:
# deployment/files/nginx.conf
server {
location / {
proxy_pass http://website:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0;
}
location /media/protected/ {
internal; # This location is only accessible via internal redirects
alias /media/protected/;
}
}
This approach provides the best of both worlds:
- Django handles authentication and access control
- nginx efficiently serves the actual files
- The
internal
directive ensures files can't be accessed directly

Hosting considerations
The website runs on Hetzner Cloud, which I've grown to love for its reliability and pricing. Initially, I considered using S3 for media storage to ensure stable downloads, but the cost analysis was clear:
- S3 traffic costs: ~€40/month for 500GB outbound traffic
- Hetzner included traffic: 20TB/month included
For a kindergarten website, Hetzner's generous traffic allowance made much more sense financially.
Lessons learned
Building a custom solution instead of reaching for WordPress taught me several valuable lessons:
- Match complexity to requirements: Not every website needs a full CMS
- Leverage existing platforms: Using Instagram for dynamic content was more effective than building a custom news system
- Security through architecture: The X-Accel-Redirect pattern elegantly solves the authenticated file serving challenge
- Cost-conscious hosting: Understanding traffic patterns helps choose the right hosting solution
The kindergarten website demonstrates that sometimes the best solution is the simplest one that meets your specific needs.