luispal.com: Django on Google App Engine

Background

luispal.com is my personal website and technical blog, built to serve as both a portfolio and a writing platform. The goal was a production-grade setup that mirrors professional standards: secure secret management, environment-aware configuration, static asset optimization, and a fully automated deployment pipeline, all without managing any servers directly.

Architecture Overview

  • Django 5 application served via Gunicorn on Google App Engine Standard (Python 3.12)
  • SQLite database for content storage, suitable for a low-write personal site
  • Static files collected with collectstatic and served directly by App Engine handlers
  • Secrets managed through Google Cloud Secret Manager, never stored in source control
  • DNS and traffic proxied through Cloudflare, providing DDoS protection, edge caching, and SSL termination between the browser and origin
  • CI/CD pipeline via GitHub Actions, deploying automatically on merge to main
luispal.com architecture diagram

Google App Engine Deployment

App Engine Standard was chosen because it handles infrastructure entirely: scaling, patching, and runtime management are all abstracted away. For a personal site with variable traffic, this means zero cost during idle periods and automatic scaling when traffic spikes, without any manual intervention.

The app.yaml configuration defines Python 3.12 as the runtime, with Gunicorn as the WSGI server entry point. Static and media file routes are declared as App Engine handlers so those requests are served at the CDN layer rather than hitting the Django application at all. The GOOGLE_CLOUD_PROJECT environment variable is set here so the application knows which GCP project to target when fetching secrets at runtime.

Environment detection is handled inside settings.py by checking the GAE_ENV variable, which App Engine sets automatically in production. When that variable is present, the application fetches the Django secret key from Secret Manager, enforces HTTPS-only cookies, and locks ALLOWED_HOSTS to the production domains. Locally, those same settings are loaded from a .env file via python-decouple, keeping the local and production configurations cleanly separated with no manual switching required.

ManifestStaticFilesStorage is used for static file serving, which appends a content hash to each filename at build time. This gives static assets permanent cache headers without the risk of stale files reaching users after a deployment.

CI/CD Pipeline

Deployments are fully automated through a GitHub Actions workflow that triggers on every merge to the main branch. The pipeline authenticates to Google Cloud using Workload Identity Federation, which eliminates the need to store long-lived service account keys as GitHub secrets.

Once authenticated, the workflow runs collectstatic to compile and hash all static assets before deploying. This ensures the version of static files served by App Engine handlers always matches the deployed application code. The final step runs gcloud app deploy, which packages the application and promotes the new version to receive traffic automatically.

Secret Manager integration means no secrets ever touch the CI/CD environment directly. The GitHub Actions runner only needs the IAM permissions to deploy. The application itself pulls the Django secret key from Secret Manager at startup, entirely separate from the deployment process.

Security Considerations

Several layers of security are configured explicitly rather than left to defaults. CSRF_COOKIE_SECURE and SESSION_COOKIE_SECURE are both enforced in production, ensuring authentication and session cookies are only transmitted over HTTPS. Traffic passes through Cloudflare in proxied mode before reaching App Engine, providing DDoS protection and WAF filtering at the edge. Cloudflare terminates TLS from the browser and opens a separate verified TLS connection to the App Engine origin using Full (strict) mode, meaning a valid Google-managed certificate is required on the origin at all times.

SECURE_PROXY_SSL_HEADER is set to trust the X-Forwarded-Proto header that App Engine injects, so Django correctly identifies all incoming requests as HTTPS. A custom middleware layer blocks unsupported HTTP methods at the application level, reducing the attack surface for unexpected request types. CSRF trusted origins are explicitly scoped to the production domains and the App Engine default URL, preventing cross-origin form submissions from any other source.

Outcome

The result is a fully serverless personal site that requires no infrastructure maintenance. Deployments are a single pull request merge. Secrets are never exposed in code or CI environments. Static assets are cache-optimized automatically on every deploy. The setup is production-grade end to end, which was the point. Building something small the right way is better practice than building something large the wrong way.