This application supports multiple storage backends via django-storages.
You can configure which storage backend to use through environment variables.
- local - Local filesystem storage (default)
- s3 - Amazon S3 or S3-compatible services (MinIO, DigitalOcean Spaces, Wasabi, Linode, Backblaze B2, etc.)
- azure - Microsoft Azure Blob Storage
- gcs - Google Cloud Storage
- sftp - SFTP/SSH Storage
- dropbox - Dropbox Storage
Warning
Only the local and s3 storage backends were tested and confirmed to be working.
Other backends should work but are subject to a proper django-storages configuration. You may have to tweak the CSP whitelist using CSP_IMG_SRC_EXTRA.
Set the STORAGE_BACKEND environment variable to the desired backend. If not set, it defaults to local.
No additional configuration needed. Files are stored in the media/ directory.
STORAGE_BACKEND=localSTORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_STORAGE_BUCKET_NAME=your_bucket_nameAWS_S3_REGION_NAME=us-east-1 # Default: us-east-1
AWS_S3_CUSTOM_DOMAIN=cdn.example.com # For CloudFront or custom CDN
AWS_S3_ENDPOINT_URL=https://minio.example.com # For S3-compatible services
AWS_DEFAULT_ACL=private # Default: private
AWS_QUERYSTRING_AUTH=True # Use signed URLs (default: True)
AWS_S3_FILE_OVERWRITE=False # Overwrite files with same name (default: False)
AWS_LOCATION=media # Folder within bucket (default: media)
AWS_S3_USE_ACCELERATE_ENDPOINT=False # Enable S3 Transfer Acceleration (default: False)STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_STORAGE_BUCKET_NAME=memelord
AWS_S3_ENDPOINT_URL=http://127.0.0.1:9000STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_do_spaces_key
AWS_SECRET_ACCESS_KEY=your_do_spaces_secret
AWS_STORAGE_BUCKET_NAME=your_space_name
AWS_S3_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com
AWS_S3_REGION_NAME=nyc3STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_wasabi_key
AWS_SECRET_ACCESS_KEY=your_wasabi_secret
AWS_STORAGE_BUCKET_NAME=memelord-bucket
AWS_S3_ENDPOINT_URL=https://s3.wasabisys.com
AWS_S3_REGION_NAME=us-east-1STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_linode_key
AWS_SECRET_ACCESS_KEY=your_linode_secret
AWS_STORAGE_BUCKET_NAME=memelord-bucket
AWS_S3_ENDPOINT_URL=https://us-east-1.linodeobjects.com
AWS_S3_REGION_NAME=us-east-1STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_b2_key_id
AWS_SECRET_ACCESS_KEY=your_b2_application_key
AWS_STORAGE_BUCKET_NAME=memelord-bucket
AWS_S3_ENDPOINT_URL=https://s3.us-west-001.backblazeb2.com
AWS_S3_REGION_NAME=us-west-001STORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_STORAGE_BUCKET_NAME=memelord-bucket
AWS_S3_REGION_NAME=us-east-1
AWS_S3_CUSTOM_DOMAIN=d1234567890.cloudfront.netSTORAGE_BACKEND=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_STORAGE_BUCKET_NAME=memelord-bucket
AWS_S3_REGION_NAME=us-east-1
AWS_S3_USE_ACCELERATE_ENDPOINT=TrueSTORAGE_BACKEND=azure
AZURE_ACCOUNT_NAME=your_account_name
AZURE_ACCOUNT_KEY=your_account_keyAZURE_CONTAINER=media # Default: media
AZURE_SSL=True # Use SSL (default: True)
AZURE_UPLOAD_MAX_CONN=2 # Max connections (default: 2)
AZURE_CONNECTION_TIMEOUT_SECS=20 # Connection timeout (default: 20)
AZURE_BLOB_MAX_MEMORY_SIZE=2MB # Max memory size (default: 2MB)
AZURE_URL_EXPIRATION_SECS=3600 # URL expiration (default: 3600)
AZURE_OVERWRITE_FILES=False # Overwrite files (default: False)
AZURE_LOCATION= # Optional prefix path
AZURE_CUSTOM_DOMAIN=cdn.example.com # Optional CDN domainSTORAGE_BACKEND=azure
AZURE_ACCOUNT_NAME=mymemelordaccount
AZURE_ACCOUNT_KEY=your_azure_key_here
AZURE_CONTAINER=mediaSTORAGE_BACKEND=gcs
GS_BUCKET_NAME=your_bucket_name
GS_PROJECT_ID=your_project_id
GS_CREDENTIALS=/path/to/credentials.jsonGS_DEFAULT_ACL=private # Default: private
GS_FILE_OVERWRITE=False # Overwrite files (default: False)
GS_LOCATION=media # Folder within bucket (default: media)
GS_CUSTOM_ENDPOINT=https://cdn.example.com # Custom domain
GS_QUERYSTRING_AUTH=True # Use signed URLs (default: True)STORAGE_BACKEND=gcs
GS_BUCKET_NAME=memelord-bucket
GS_PROJECT_ID=memelord-project
GS_CREDENTIALS=/app/gcs-credentials.json
GS_LOCATION=mediaSTORAGE_BACKEND=sftp
SFTP_STORAGE_HOST=sftp.example.com
SFTP_STORAGE_USERNAME=your_username# Password authentication:
SFTP_STORAGE_PASSWORD=your_password
# OR Private key authentication:
SFTP_STORAGE_PRIVATE_KEY=/path/to/private_keySFTP_STORAGE_PORT=22 # Default: 22
SFTP_STORAGE_ROOT=/media/ # Root path (default: /media/)
SFTP_STORAGE_INTERACTIVE=False # Interactive mode (default: False)
SFTP_STORAGE_FILE_MODE=0644 # File permissions
SFTP_STORAGE_DIR_MODE=0755 # Directory permissions
SFTP_STORAGE_UID=1000 # User ID
SFTP_STORAGE_GID=1000 # Group ID
SFTP_KNOWN_HOST_FILE=/path/to/known_hosts # Known hosts file
SFTP_CUSTOM_DOMAIN=cdn.example.com # If serving via nginx/apache on separate domainSTORAGE_BACKEND=sftp
SFTP_STORAGE_HOST=sftp.example.com
SFTP_STORAGE_PORT=22
SFTP_STORAGE_USERNAME=memelord
SFTP_STORAGE_PASSWORD=secure_password
SFTP_STORAGE_ROOT=/var/www/media/STORAGE_BACKEND=dropbox
DROPBOX_OAUTH2_TOKEN=your_oauth2_tokenDROPBOX_ROOT_PATH=/media # Root path (default: /media)
DROPBOX_TIMEOUT=100 # Timeout in seconds (default: 100)
DROPBOX_WRITE_MODE=add # 'add' or 'overwrite' (default: add)STORAGE_BACKEND=dropbox
DROPBOX_OAUTH2_TOKEN=your_long_oauth2_token_here
DROPBOX_ROOT_PATH=/Apps/MemeLord/media- Create an app at https://www.dropbox.com/developers/apps
- Generate an access token in the app settings
- Use that token as
DROPBOX_OAUTH2_TOKEN
A Django management command is available to test and debug your storage backend configuration:
python manage.py test_storageThis command will:
- Display your current storage backend configuration
- Show all relevant environment variables
- Test file upload, retrieval, and deletion
- Verify the storage backend is working correctly
- Display any errors with detailed tracebacks
=== Storage Backend Test ===
Storage Backend: s3
Storage Class: S3Boto3Storage
Media URL: https://my-bucket.s3.amazonaws.com/media/
--- S3 Configuration ---
AWS_ACCESS_KEY_ID: SET
AWS_SECRET_ACCESS_KEY: SET
AWS_STORAGE_BUCKET_NAME: my-bucket
AWS_S3_REGION_NAME: us-east-1
AWS_S3_ENDPOINT_URL: DEFAULT (AWS)
AWS_LOCATION: media
AWS_DEFAULT_ACL: private
AWS_QUERYSTRING_AUTH: True
AWS_S3_FILE_OVERWRITE: False
--- Testing File Upload ---
Attempting to save test file: memes/user_test/test_storage_file.txt
✓ File saved successfully: media/memes/user_test/test_storage_file.txt
✓ File exists in storage
✓ File URL: https://my-bucket.s3.amazonaws.com/media/memes/user_test/test_storage_file.txt
✓ File size: 54 bytes
✓ File content verified
=== Test Complete ===
Set the following environment variables to enable detailed logging:
DEBUG=True
DJANGO_LOG_LEVEL=DEBUGThis will enable detailed logging for:
- Django framework operations
- Storage backend operations (django-storages)
- AWS SDK operations (boto3/botocore for S3)
- Azure SDK operations
- Google Cloud SDK operations
Logs are written to both console and file:
- Console: Real-time output when running Django
- File:
logs/django.log(with automatic rotation, max 15MB per file)
# Tail the log file
tail -f logs/django.log
# Search for storage-related errors
grep -i "storage\|s3\|boto" logs/django.log
# Search for file upload operations
grep -i "saving media file\|upload" logs/django.logSuccessful Upload:
INFO Saving media file: memes/user_1/example.jpg using storage backend: s3
DEBUG Storage class: S3Boto3Storage
DEBUG File size: 524288 bytes
DEBUG S3 Bucket: my-bucket
DEBUG S3 Region: us-east-1
INFO Media file saved successfully: media/memes/user_1/example.jpg
DEBUG File URL: https://my-bucket.s3.amazonaws.com/media/memes/user_1/example.jpg?...
Upload Error:
ERROR Error during storage test: An error occurred (NoSuchBucket) when calling the PutObject operation
- Set up your cloud storage bucket/container with appropriate permissions
- Configure environment variables for your chosen backend
- Upload existing media files to the new storage backend (manual migration required)
- Update your environment and restart the application
# Using AWS CLI to sync local media to S3
aws s3 sync ./media/ s3://your-bucket-name/media/ --acl privateFor cloud-native deployments with horizontal scaling, you can configure Redis for session storage and general caching. This ensures sessions work across all application instances.
Benefits:
- ✅ Horizontal Scaling - Sessions work across multiple app instances/containers
- ✅ Fast Performance - Redis is in-memory, much faster than database queries
- ✅ Load Balancer Friendly - Users stay logged in when routed to different servers
- ✅ Automatic Expiration - Redis handles session cleanup automatically
- ✅ Cloud Native - Works perfectly with Kubernetes, Docker Swarm, ECS, etc.
REDIS_HOST=redis.example.com # Redis server hostnameREDIS_PORT=6379 # Default: 6379
REDIS_DB=0 # Default: 0
REDIS_PASSWORD=your_password # Optional: Redis passwordREDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0REDIS_HOST=memelord.abc123.0001.use1.cache.amazonaws.com
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=REDIS_HOST=memelord.redis.cache.windows.net
REDIS_PORT=6380
REDIS_DB=0
REDIS_PASSWORD=your_access_keyREDIS_HOST=10.0.0.3
REDIS_PORT=6379
REDIS_DB=0services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
web:
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=0
volumes:
redis_data:When REDIS_HOST is set:
- Sessions are stored in Redis
- Fast session lookups (in-memory)
- Sessions persist across app restarts (if Redis persistence enabled)
- General caching uses Redis (with 5-minute default timeout)
When REDIS_HOST is not set (default):
- Sessions are stored in the database (backward compatible)
- No Redis connection required
- Works for single-instance deployments
# Test Redis connectivity
redis-cli -h <REDIS_HOST> -p <REDIS_PORT> ping
# Should return: PONG
# View Redis session keys
redis-cli -h <REDIS_HOST> -p <REDIS_PORT>
> KEYS memelord:*The Redis cache is configured with:
- Key Prefix:
memelord- Prevents key collisions with other apps - Timeout: 300 seconds (5 minutes) default
- Compression: zlib compression for data efficiency
- Connection Pool: Max 50 connections, with retry on timeout
- Graceful Degradation:
IGNORE_EXCEPTIONS=True- App continues if Redis is down
- Use Redis for Sessions in Production - Database sessions don't scale well
- Enable Redis Persistence - Use AOF or RDB to prevent session loss on Redis restart
- Monitor Redis Memory - Set
maxmemoryandmaxmemory-policyin Redis config - Use Connection Pooling - Already configured (50 max connections)
- Consider Redis Cluster - For very high-traffic deployments
Problem: Users getting logged out randomly
Causes:
- Redis server restarted without persistence
- Redis memory full (evicting keys)
- Network connectivity issues
Solutions:
- Enable Redis AOF persistence:
redis-server --appendonly yes - Set appropriate
maxmemory-policy:allkeys-lruorvolatile-lru - Monitor Redis with CloudWatch/Prometheus
Problem: Sessions not working after deployment
Check:
- Redis host is accessible from app containers
- Firewall rules allow Redis port (6379)
- Redis password is correct (if set)
- Test connection:
redis-cli -h $REDIS_HOST ping
When using cloud storage backends, you need to ensure your Content Security Policy allows images from the storage provider's domain. The application automatically configures CSP based on your storage backend using smart provider detection to avoid unnecessary whitelisting.
The application automatically adds the appropriate domains to the CSP img-src directive based on your STORAGE_BACKEND setting. Importantly, it only whitelists the specific provider you're using, not all possible providers.
Provider Detection Logic:
- If
AWS_S3_ENDPOINT_URLis NOT set → Assumes AWS S3 and whitelists AWS-specific URLs only - If
AWS_S3_ENDPOINT_URLIS set → Detects the S3-compatible provider and whitelists only that provider's URLs
For AWS S3 (when no endpoint URL is set): The application whitelists these AWS-specific formats:
https://bucket-name.s3.amazonaws.com(global endpoint)https://bucket-name.s3.region.amazonaws.com(region-specific endpoint)https://s3.amazonaws.com(path-style, legacy)https://s3.region.amazonaws.com(path-style with region)https://bucket-name.s3.dualstack.region.amazonaws.com(IPv6 support)https://bucket-name.s3-accelerate.amazonaws.com(ifAWS_S3_USE_ACCELERATE_ENDPOINT=True)https://bucket-name.s3-accelerate.dualstack.amazonaws.com(if acceleration enabled)
For S3-Compatible Services (when endpoint URL is set): The application automatically detects the provider and whitelists:
- The endpoint domain from
AWS_S3_ENDPOINT_URL - Bucket-based subdomain format:
https://bucket-name.endpoint-domain.com
Provider-Specific Additional URLs:
- DigitalOcean Spaces: Also adds CDN domain
https://bucket-name.region.cdn.digitaloceanspaces.com - Cloudflare R2: Endpoint domain only (use
AWS_S3_CUSTOM_DOMAINfor R2 public URLs) - Wasabi, Linode, Backblaze B2, MinIO: Endpoint and bucket subdomain formats
Custom CDN (CloudFront, etc.):
- If
AWS_S3_CUSTOM_DOMAINis set, that domain is always added to the whitelist
Example - Using MinIO:
AWS_S3_ENDPOINT_URL=https://minio.example.com
AWS_STORAGE_BUCKET_NAME=memelordCSP Whitelist: Only https://minio.example.com and https://memelord.minio.example.com are added. AWS endpoints are NOT whitelisted.
Example - Using AWS S3:
# No AWS_S3_ENDPOINT_URL set
AWS_STORAGE_BUCKET_NAME=memelord
AWS_S3_REGION_NAME=us-west-2CSP Whitelist: Only AWS S3 URLs are added. MinIO/DigitalOcean/etc. endpoints are NOT whitelisted.
Azure Blob Storage URLs:
https://account-name.blob.core.windows.net
Custom CDN:
- The domain specified in
AZURE_CUSTOM_DOMAIN
GCS URLs (all common formats):
https://storage.googleapis.comhttps://storage.cloud.google.comhttps://bucket-name.storage.googleapis.com
Custom endpoint or CDN:
- The domain specified in
GS_CUSTOM_ENDPOINT
Default behavior: Files are retrieved by Django and served through Django views using the 'self' origin (already in CSP whitelist). No additional CSP configuration is needed.
Optional: If you have a separate web server (nginx/apache) serving the SFTP directory via HTTP, you can whitelist that domain:
# For SFTP
SFTP_CUSTOM_DOMAIN=cdn.example.comFor edge cases, custom CDNs, or domains not automatically detected, you can manually add additional domains to the CSP img-src whitelist:
Environment Variable:
CSP_IMG_SRC_EXTRA=https://cdn.example.com,https://cdn2.example.com,https://custom-domain.netThis is useful for:
- Custom CDN configurations not detected automatically
- Multiple CDN providers
- Proxy servers or image transformation services
- Migration scenarios where you need to support multiple storage backends temporarily
Example: AWS S3 with Multiple CDNs:
STORAGE_BACKEND=s3
AWS_STORAGE_BUCKET_NAME=memelord
AWS_S3_REGION_NAME=us-east-1
AWS_S3_CUSTOM_DOMAIN=d1234567890.cloudfront.net
CSP_IMG_SRC_EXTRA=https://cdn.example.com,https://backup-cdn.example.comThis configuration will whitelist:
- AWS S3 URLs (auto-detected)
- CloudFront domain (from
AWS_S3_CUSTOM_DOMAIN) - Your two custom CDN domains (from
CSP_IMG_SRC_EXTRA)
AWS S3 supports two URL formats:
- Virtual-hosted-style (recommended):
https://bucket-name.s3.region.amazonaws.com/path/to/file - Path-style (legacy):
https://s3.region.amazonaws.com/bucket-name/path/to/file
The application uses virtual-hosted-style URLs by default (via AWS_S3_ADDRESSING_STYLE = 'virtual'). This is important because:
- Virtual-hosted-style URLs work with the automatic CSP configuration
- Path-style URLs are being deprecated by AWS
- Signed URLs must use the same format as the endpoint configuration
The application whitelists BOTH formats to ensure maximum compatibility.
Azure uses the format: https://account-name.blob.core.windows.net/container-name/path/to/file
GCS supports multiple URL formats:
- Canonical:
https://storage.googleapis.com/bucket-name/path/to/file - XML API:
https://bucket-name.storage.googleapis.com/path/to/file
The application whitelists ALL common GCS URL formats automatically.
Problem: Images not loading, browser console shows CSP violation
Solution: Check that:
- Your storage backend environment variables are set correctly
- The URL format in the CSP error matches your storage configuration
- Restart Django after changing storage backend settings
Example CSP Error:
Content-Security-Policy: The page's settings blocked the loading of a resource (img-src)
at https://example.s3.amazonaws.com/... because it violates the following directive: "img-src 'self'..."
This means the storage URL is not in the CSP allowlist. Verify your STORAGE_BACKEND environment variable is set and restart Django.
The application automatically detects and whitelists domains for all major S3-compatible providers:
| Provider | Automatic CSP Support | Example Endpoint |
|---|---|---|
| AWS S3 | ✅ Yes | Automatic (no endpoint needed) |
| MinIO | ✅ Yes | https://minio.example.com |
| DigitalOcean Spaces | ✅ Yes | https://nyc3.digitaloceanspaces.com |
| Wasabi | ✅ Yes | https://s3.wasabisys.com |
| Linode Object Storage | ✅ Yes | https://us-east-1.linodeobjects.com |
| Backblaze B2 | ✅ Yes | https://s3.us-west-001.backblazeb2.com |
| Cloudflare R2 | ✅ Yes | https://account-id.r2.cloudflarestorage.com |
| Any S3-compatible | ✅ Yes | Set via AWS_S3_ENDPOINT_URL |
All endpoint URLs are automatically parsed and added to the CSP img-src directive along with bucket-based subdomain formats.
When using private storage (AWS_DEFAULT_ACL=private, GS_DEFAULT_ACL=private, etc.), the storage backends generate signed URLs with temporary access credentials.
S3 signed URLs include query parameters for authentication:
https://bucket.s3.region.amazonaws.com/path/file.jpg?
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20251209/us-east-1/s3/aws4_request&
X-Amz-Date=20251209T120000Z&
X-Amz-Expires=3600&
X-Amz-SignedHeaders=host&
X-Amz-Signature=...
Important: The signature is region-specific. If you get 403 Forbidden errors:
- Ensure
AWS_S3_REGION_NAMEmatches your bucket's region - Don't manually set
AWS_S3_ENDPOINT_URLfor standard AWS S3 (only for S3-compatible services) - Use Signature Version 4 (
AWS_S3_SIGNATURE_VERSION = 's3v4') - this is configured automatically
Azure generates SAS tokens for private blob access:
https://account.blob.core.windows.net/container/path/file.jpg?
sv=2020-08-04&
st=2025-12-09T12:00:00Z&
se=2025-12-09T13:00:00Z&
sr=b&
sp=r&
sig=...
Google Cloud Storage signed URLs use similar authentication:
https://storage.googleapis.com/bucket/path/file.jpg?
X-Goog-Algorithm=GOOG4-RSA-SHA256&
X-Goog-Credential=...&
X-Goog-Date=20251209T120000Z&
X-Goog-Expires=3600&
X-Goog-SignedHeaders=host&
X-Goog-Signature=...
Signed URLs expire after a certain time (default 1 hour for S3). Configure expiration times:
- S3: Not directly configurable via django-storages (uses 3600 seconds)
- Azure:
AZURE_URL_EXPIRATION_SECS=3600 - GCS: Not directly configurable via django-storages (uses 3600 seconds)
For longer expiration times or public access, consider setting ACLs to public:
- S3:
AWS_DEFAULT_ACL=public-read - GCS:
GS_DEFAULT_ACL=publicRead - Azure: Use public containers or connection strings
- Never commit credentials to version control
- Use environment variables or secret management services
- Use private ACLs for sensitive content
- Enable HTTPS for all cloud storage endpoints
- Rotate access keys regularly
- Use IAM roles instead of access keys when running on cloud platforms
- Enable versioning on your cloud storage buckets
- Set up proper CORS policies if accessing files from browsers
- Enable bucket encryption at rest (AWS SSE, Azure Storage Service Encryption, GCS CMEK)
- Use S3 Transfer Acceleration for faster uploads from distant geographic regions
- Monitor storage access logs for suspicious activity