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.

Kindergarten website homepage

Why not WordPress?

The decision to avoid WordPress was deliberate, based on three key considerations:

  1. Simplicity focus: The website primarily serves static content - basic information about the kindergarten, contact details, and educational philosophy. A full CMS would be overkill.

  2. 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.

  3. Secure file sharing: The most important feature is secure distribution of photos and videos between educators and parents through a protected download system.

Website content structure

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
Secure download functionality

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:

  1. Match complexity to requirements: Not every website needs a full CMS
  2. Leverage existing platforms: Using Instagram for dynamic content was more effective than building a custom news system
  3. Security through architecture: The X-Accel-Redirect pattern elegantly solves the authenticated file serving challenge
  4. 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.