Deploying a Django application to production involves more than just
writing good code. It requires thinking about infrastructure, security,
automation, and reliability. In this post I walk through the complete
production deployment stack I built for my portfolio CMS.
## The Stack
- Django + Gunicorn as the application server
- PostgreSQL as the database
- Docker and Docker Compose for containerization
- Nginx as the reverse proxy
- AWS EC2 for hosting
- AWS S3 for media file storage
- Terraform for infrastructure as code
- GitHub Actions for CI/CD
## Why Terraform?
Most tutorials show you how to click through the AWS console to create
resources. That works for learning but it doesn't scale and it's not
reproducible. With Terraform, the entire infrastructure — VPC, subnets,
security groups, EC2 instance, S3 bucket, IAM roles — is defined as code
and can be recreated in minutes with a single command.
terraform apply
That's the entire infrastructure. Version controlled. Peer reviewable.
Reproducible.
## The Docker Setup
The application runs in three containers managed by Docker Compose:
- web: Django application served by Gunicorn
- db: PostgreSQL database
- nginx: Reverse proxy handling SSL termination and static files
The Dockerfile uses a multi-stage build. The builder stage installs all
dependencies into an isolated virtual environment. The production stage
copies only the virtual environment and application code into a lean
runtime image, runs as a non-root user, and never contains build tools.
This keeps the production image small and reduces the attack surface.
## The CI/CD Pipeline
Every push to the main branch triggers two GitHub Actions workflows:
CI runs the full test suite against a real PostgreSQL database, then
builds the production Docker image and pushes it to Docker Hub.
CD connects to the EC2 instance via SSH, pulls the latest image,
restarts the containers, and verifies the deployment via a health
check endpoint.
The entire process takes about 4 minutes from git push to live deployment.
## HTTPS with Let's Encrypt
SSL certificates are issued and renewed automatically using Certbot.
A cron job runs daily and renews the certificate when it is within
30 days of expiry, then reloads Nginx to pick up the new certificate.
## What I Learned
Infrastructure is code. Treating servers as cattle rather than pets —
meaning you should be able to destroy and recreate your entire
infrastructure without losing sleep — is the mindset shift that
separates modern DevOps from traditional sysadmin work.
Security defaults should be strict. HSTS, secure cookies, CSP headers,
non-root containers, and principle of least privilege IAM roles are not
optional extras — they are the baseline.
Automation pays off immediately. The time invested in building the
CI/CD pipeline was recovered on the second deployment.
writing good code. It requires thinking about infrastructure, security,
automation, and reliability. In this post I walk through the complete
production deployment stack I built for my portfolio CMS.
## The Stack
- Django + Gunicorn as the application server
- PostgreSQL as the database
- Docker and Docker Compose for containerization
- Nginx as the reverse proxy
- AWS EC2 for hosting
- AWS S3 for media file storage
- Terraform for infrastructure as code
- GitHub Actions for CI/CD
## Why Terraform?
Most tutorials show you how to click through the AWS console to create
resources. That works for learning but it doesn't scale and it's not
reproducible. With Terraform, the entire infrastructure — VPC, subnets,
security groups, EC2 instance, S3 bucket, IAM roles — is defined as code
and can be recreated in minutes with a single command.
terraform apply
That's the entire infrastructure. Version controlled. Peer reviewable.
Reproducible.
## The Docker Setup
The application runs in three containers managed by Docker Compose:
- web: Django application served by Gunicorn
- db: PostgreSQL database
- nginx: Reverse proxy handling SSL termination and static files
The Dockerfile uses a multi-stage build. The builder stage installs all
dependencies into an isolated virtual environment. The production stage
copies only the virtual environment and application code into a lean
runtime image, runs as a non-root user, and never contains build tools.
This keeps the production image small and reduces the attack surface.
## The CI/CD Pipeline
Every push to the main branch triggers two GitHub Actions workflows:
CI runs the full test suite against a real PostgreSQL database, then
builds the production Docker image and pushes it to Docker Hub.
CD connects to the EC2 instance via SSH, pulls the latest image,
restarts the containers, and verifies the deployment via a health
check endpoint.
The entire process takes about 4 minutes from git push to live deployment.
## HTTPS with Let's Encrypt
SSL certificates are issued and renewed automatically using Certbot.
A cron job runs daily and renews the certificate when it is within
30 days of expiry, then reloads Nginx to pick up the new certificate.
## What I Learned
Infrastructure is code. Treating servers as cattle rather than pets —
meaning you should be able to destroy and recreate your entire
infrastructure without losing sleep — is the mindset shift that
separates modern DevOps from traditional sysadmin work.
Security defaults should be strict. HSTS, secure cookies, CSP headers,
non-root containers, and principle of least privilege IAM roles are not
optional extras — they are the baseline.
Automation pays off immediately. The time invested in building the
CI/CD pipeline was recovered on the second deployment.