Every production Django application that handles file uploads must store files on S3 (or equivalent object storage) — never on the web server's local disk. Local files disappear when you redeploy, when the server fails, and when you scale to multiple instances. S3 is durable (11 nines), scalable, and costs fractions of a cent per GB.
Installation
pip install django-storages boto3
AWS Setup
1. Create an S3 Bucket
In AWS Console → S3 → Create bucket:
- Bucket name:myproject-media-production (must be globally unique)
- Region: Select your closest region (e.g., ap-south-1 for India)
- Block all public access: Keep enabled by default (we will make specific files public via policy)
- Versioning: Enable for important user uploads (allows recovery from accidental deletions)
2. Create an IAM User with Least-Privilege Permissions
Create an IAM user specifically for your Django application — not your root account, not your developer account.
IAM Policy (attach to the user):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::myproject-media-production",
"arn:aws:s3:::myproject-media-production/*"
]
}
]
}
This policy grants access to only this bucket — nothing else in your AWS account.
Create access keys for this IAM user and store them in your environment file — never in code.
3. Bucket Policy for Public Read (Public Files)
If your media files should be publicly readable (profile pictures, product images):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::myproject-media-production/public/*"
}
]
}
This makes only files in the public/ prefix publicly readable. Files in private/ require authenticated access.
Django Configuration
# settings.py
import os
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
AWS_STORAGE_BUCKET_NAME = os.environ["AWS_STORAGE_BUCKET_NAME"]
AWS_S3_REGION_NAME = "ap-south-1"
AWS_S3_SIGNATURE_VERSION = "s3v4"
# Cache headers for browser caching
AWS_S3_OBJECT_PARAMETERS = {
"CacheControl": "max-age=86400", # 24 hours
}
# Prevent overwriting existing files
AWS_S3_FILE_OVERWRITE = False
# Static files
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
AWS_STATIC_LOCATION = "static"
STATIC_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/"
# Media files
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_MEDIA_LOCATION = "media"
MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/"
Separate Storage Classes for Public vs Private Files
For applications with both public files (profile pictures) and private files (invoices, medical records):
# storages.py
from storages.backends.s3boto3 import S3Boto3Storage
class PublicMediaStorage(S3Boto3Storage):
location = "public"
default_acl = "public-read"
file_overwrite = False
class PrivateMediaStorage(S3Boto3Storage):
location = "private"
default_acl = "private"
file_overwrite = False
custom_domain = False # Force signed URLs
Use in models:
from .storages import PublicMediaStorage, PrivateMediaStorage
class UserProfile(models.Model):
avatar = models.ImageField(
storage=PublicMediaStorage(),
upload_to="avatars/",
)
id_document = models.FileField(
storage=PrivateMediaStorage(),
upload_to="documents/",
)
Private File Access with Pre-Signed URLs
Private files require authenticated access. Generate a time-limited URL:
import boto3
from django.conf import settings
def get_private_file_url(file_key: str, expiry_seconds: int = 3600) -> str:
s3_client = boto3.client(
"s3",
region_name=settings.AWS_S3_REGION_NAME,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": file_key,
},
ExpiresIn=expiry_seconds,
)
return url
The URL is valid for 1 hour (3600 seconds). After that, it returns 403. Generate a fresh URL for each download request.
CloudFront CDN for Faster File Delivery
S3 is in one region. Users far from that region experience higher latency. CloudFront serves files from edge locations worldwide — files cached near the user load in milliseconds instead of hundreds of milliseconds.
Create a CloudFront Distribution
- AWS Console → CloudFront → Create distribution
- Origin domain: Select your S3 bucket
- Origin access: "Origin access control settings (recommended)" — this prevents direct S3 access, forcing all traffic through CloudFront
- Cache policy:
CachingOptimizedfor media files - Price class: Choose based on your user geography (all regions = higher cost)
Update Django Settings to Use CloudFront
AWS_S3_CUSTOM_DOMAIN = "d1234567890abc.cloudfront.net"
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
Files are now served via CloudFront edge locations globally, with S3 as the origin.
Custom Domain for CloudFront
Instead of the d123.cloudfront.net domain, use media.yoursite.com:
- Create an ACM certificate for
media.yoursite.comin us-east-1 (required for CloudFront) - Add the certificate to the CloudFront distribution
- Add a CNAME DNS record:
media.yoursite.com → d1234567890abc.cloudfront.net
AWS_S3_CUSTOM_DOMAIN = "media.yoursite.com"
Deploying collectstatic
python manage.py collectstatic --no-input
django-storages uploads all static files to S3 automatically. Run this as part of every deployment.
Environment Variables Summary
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_STORAGE_BUCKET_NAME=myproject-media-production
Keep these out of version control. Set them in your deployment environment (systemd EnvironmentFile, Docker secrets, or GitHub Actions secrets).
If you need a Django application with S3 file storage, CloudFront CDN, and proper IAM permissions configured, hire me as an AWS developer or as a Django developer who handles the full stack from code to cloud.