Production-ready Django deployment with PostgreSQL, Redis, and automated migrations.
- 🐍 Python 3.9-3.12 support
- 🗄️ PostgreSQL database integration
- 🚀 Redis cache support (optional)
- 🔒 Security best practices (non-root user, secrets management)
- 📦 Multi-stage Docker builds
- 🔄 Automatic database migrations
- 📊 Health checks
- 🎨 Static file collection
- ⚡ Gunicorn WSGI server with configurable workers
- 👤 Automatic superuser creation
arfni plugin install django- Settings > Plugins
- Enter "django" in the install box
- Click Install
- Create Django project:
django-admin startproject myproject
cd myproject- Initialize ARFNI:
arfni init- Configure stack.yaml:
apiVersion: v0.1
name: my-django-app
targets:
local:
type: docker-desktop
services:
api:
kind: docker.container
target: local
spec:
build: .
ports: ["8000:8000"]
env:
SECRET_KEY: ${DJANGO_SECRET_KEY}
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/mydb- Set environment variables:
# .env
DJANGO_SECRET_KEY=your-secret-key-here
POSTGRES_PASSWORD=your-postgres-password- Deploy:
arfni applyYour Django app will be available at http://localhost:8000
| Variable | Required | Default | Description |
|---|---|---|---|
DJANGO_SECRET_KEY |
Yes | - | Django secret key for cryptographic signing |
POSTGRES_PASSWORD |
Yes | - | PostgreSQL database password |
DJANGO_SUPERUSER_USERNAME |
No | admin |
Admin username |
DJANGO_SUPERUSER_EMAIL |
No | admin@localhost |
Admin email |
DJANGO_SUPERUSER_PASSWORD |
No | auto-generated | Admin password |
Configure via plugin.yaml or ARFNI GUI:
inputs:
python_version: "3.11"
django_port: 8000
postgres_db: "mydb"
enable_redis: true
debug_mode: false
gunicorn_workers: 4This plugin adds three services to your stack:
- Image: Custom built from your Django project
- Port: 8000 (configurable)
- Health Check: HTTP GET
/health/ - Volumes:
./media,./static - Dependencies: PostgreSQL, Redis (optional)
- Image:
postgres:15-alpine - Port: 5432
- Volume:
postgres-data(persistent) - Health Check: TCP port 5432
- Image:
redis:7-alpine - Port: 6379
- Volume:
redis-data(persistent) - Health Check: TCP port 6379
The plugin executes hooks at different stages:
- Validates Django project structure
- Checks for required files (
manage.py,requirements.txt)
- Collects Django static files
- Prepares assets for production
- Waits for PostgreSQL to be ready
- Runs Django database migrations
- Shows migration status
- Creates Django superuser if needed
- Uses
DJANGO_SUPERUSER_*environment variables - Generates random password if not provided
- Verifies Django application health
- Checks database connectivity
- Validates Redis connection (if enabled)
- Reports unapplied migrations
Your Django project must have:
- ✅
manage.py- Django management script - ✅
requirements.txt- Python dependencies - ✅
settings.py- Django settings - ✅
wsgi.pyorasgi.py- WSGI/ASGI application
Django>=4.2,<5.0
psycopg2-binary>=2.9
gunicorn>=20.1
python-dotenv>=1.0
redis>=4.5 # if using Redis# stack.yaml
apiVersion: v0.1
name: django-blog
targets:
local:
type: docker-desktop
services:
blog:
kind: docker.container
target: local
spec:
build: .
ports: ["8000:8000"]services:
api:
kind: docker.container
spec:
build: .
ports: ["8000:8000"]
env:
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/mydb
postgres:
kind: docker.container
spec:
image: postgres:15-alpine
env:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
- postgres-dataservices:
api:
kind: docker.container
spec:
build: .
env:
DEBUG: "False"
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/mydb
REDIS_URL: redis://redis:6379/0
ALLOWED_HOSTS: example.com,www.example.com
postgres:
kind: docker.container
spec:
image: postgres:15-alpine
redis:
kind: docker.container
spec:
image: redis:7-alpine
nginx:
kind: proxy.nginx # Nginx plugin required
spec:
upstreams:
- service: api
port: 8000
ssl:
enabled: trueIf you need a custom Dockerfile, create one in your project root. The plugin will detect it and skip generating a new one.
Create settings_production.py for production-specific settings:
from .settings import *
DEBUG = False
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*').split(',')
# Database
DATABASES = {
'default': dj_database_url.config(
default=os.getenv('DATABASE_URL'),
conn_max_age=600
)
}
# Cache (Redis)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.getenv('REDIS_URL', 'redis://redis:6379/0'),
}
}
# Static files
STATIC_ROOT = '/app/static/'
MEDIA_ROOT = '/app/media/'Override any hook by creating a script in your project:
# .arfni/hooks/pre-deploy.sh
#!/bin/bash
echo "Running custom pre-deploy hook"
python manage.py migrate
python manage.py loaddata initial_data.jsonProblem: Django can't connect to PostgreSQL
Solution:
- Check PostgreSQL logs:
docker compose logs postgres- Verify environment variables:
docker compose config | grep DATABASE- Ensure PostgreSQL is ready before Django starts (handled by hooks)
Problem: Migrations fail during deployment
Solution:
- Check Django logs:
docker compose logs django- Run migrations manually:
docker compose run --rm django python manage.py migrate --noinput- Check for migration conflicts:
docker compose run --rm django python manage.py showmigrationsProblem: CSS/JS files not loading
Solution:
- Collect static files manually:
docker compose run --rm django python manage.py collectstatic --noinput- Check volume mounts:
docker compose exec django ls -la /app/static- Verify Nginx configuration (if using Nginx plugin)
Problem: Permission errors when writing to volumes
Solution:
The Dockerfile creates a non-root django user (UID 1000). Ensure your local directories match:
sudo chown -R 1000:1000 ./media ./staticAdd a health check view to your Django project:
# urls.py
from django.http import JsonResponse
from django.db import connection
def health_check(request):
# Check database
try:
connection.ensure_connection()
db_status = "healthy"
except Exception as e:
db_status = f"unhealthy: {str(e)}"
return JsonResponse({
"status": "healthy" if db_status == "healthy" else "unhealthy",
"database": db_status,
})
urlpatterns = [
path('health/', health_check),
# ... other URLs
]Formula: (2 * CPU_CORES) + 1
inputs:
gunicorn_workers: 9 # For 4-core machineAdd to settings.py:
DATABASES = {
'default': {
# ... other settings
'CONN_MAX_AGE': 600, # 10 minutes
'OPTIONS': {
'connect_timeout': 10,
}
}
}CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.getenv('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {'max_connections': 50}
}
}
}- ARFNI: >=0.2.0
- Docker: >=20.10
- Docker Compose: >=2.0
- Python: 3.9-3.12
- Django: >=3.2
MIT License - see LICENSE file for details.
Contributions welcome! Please see CONTRIBUTING.md.
- Documentation: https://docs.arfni.dev/plugins/django
- Issues: https://github.com/arfni/arfni-plugins/issues
- Discussions: https://github.com/arfni/arfni/discussions
See CHANGELOG.md for version history.
Made with ❤️ by the ARFNI Community