Skip to main content

Production Deployment

Complete guide to deploying your Next.js admin to production.

Overview

The Next.js admin integration is designed for zero-hassle production deployment:

  • ZIP-based - Single file instead of thousands
  • Auto-extraction - Extracts on first request
  • Docker-optimized - Minimal image size
  • No collectstatic - WhiteNoise serves directly
  • CDN-ready - Optional CDN integration

Production Checklist

Before deploying, ensure:

  • Next.js admin built (confirm 'Y' when prompted or use --no-build and build manually)
  • ZIP archive exists: static/nextjs_admin.zip
  • Environment variables configured
  • CORS settings for production domain
  • SECRET_KEY set from environment
  • DEBUG=False in production

Deployment Methods

Docker Deployment

1. Build Next.js Admin

First, generate API clients and build Next.js:

# Generate TypeScript clients and build
python manage.py generate_clients --typescript

# Verify ZIP was created
ls -lh static/nextjs_admin.zip
# Should show ~5-10MB file

2. Create Dockerfile

Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy Django application
COPY . .

# Copy ZIP archives (NOT extracted directories!)
COPY static/frontend/admin.zip /app/static/frontend/
COPY static/nextjs_admin.zip /app/static/

# Collect static files (optional, WhiteNoise serves from STATICFILES_DIRS)
# RUN python manage.py collectstatic --noinput

# Expose port
EXPOSE 8000

# Run application
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "core.wsgi:application"]
Image Size Optimization

Copying ZIP files instead of extracted directories reduces image size by ~60%.

Before (extracted):

COPY static/frontend/admin/ /app/static/frontend/admin/  # ~20MB, 5000+ files

After (ZIP):

COPY static/frontend/admin.zip /app/static/frontend/  # ~7MB, 1 file

3. Build Docker Image

# Build image
docker build -t myapp:latest .

# Check image size
docker images myapp:latest
# REPOSITORY TAG SIZE
# myapp latest 450MB (with ZIP files)

4. Run Container

docker run -d \
--name myapp \
-p 8000:8000 \
-e DEBUG=False \
-e SECRET_KEY="your-secret-key" \
-e DATABASE_URL="postgresql://..." \
myapp:latest

5. Verify Deployment

# Check logs for ZIP extraction
docker logs myapp

# Should see:
# INFO: Extracting admin.zip to static/frontend/admin/...
# INFO: Successfully extracted admin.zip
# INFO: Extracting nextjs_admin.zip to static/nextjs_admin/...
# INFO: Successfully extracted nextjs_admin.zip

# Test endpoint
curl http://localhost:8000/admin/

Docker Compose Example

docker-compose.yml
version: '3.8'

services:
web:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=${DATABASE_URL}
- ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
- CORS_ALLOWED_ORIGINS=https://yourdomain.com
volumes:
- static_data:/app/static_extracted
depends_on:
- db

db:
image: postgres:15
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
static_data: # Persists extracted files across restarts
# Run with docker-compose
docker-compose up -d

# Check status
docker-compose ps

# View logs
docker-compose logs -f web

Environment Configuration

Production Settings

api/config.py
import os

config = DjangoConfig(
# Environment mode
env_mode=os.getenv("ENV_MODE", "production"),

# Security
secret_key=os.getenv("SECRET_KEY"),
debug=os.getenv("DEBUG", "False") == "True",
allowed_hosts=os.getenv("ALLOWED_HOSTS", "").split(","),

# Next.js Admin
nextjs_admin=NextJsAdminConfig(
project_path=os.getenv("NEXTJS_ADMIN_PATH", "../django_admin"),
static_url="/admin-ui/",
),

# CORS
cors_allowed_origins=os.getenv("CORS_ALLOWED_ORIGINS", "").split(","),

# Database
databases={
"default": DatabaseConfig(
url=os.getenv("DATABASE_URL"),
)
},
)

Environment Variables

.env.production
# Django
ENV_MODE=production
DEBUG=False
SECRET_KEY=your-long-random-secret-key
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

# Next.js Admin
NEXTJS_ADMIN_PATH=/app/django_admin

# CORS
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

# Database
DATABASE_URL=postgresql://user:password@host:5432/dbname

Performance Optimization

1. WhiteNoise Configuration

WhiteNoise is auto-configured by django-cfg, but you can optimize:

api/config.py
config = DjangoConfig(
# WhiteNoise is enabled by default
# Serves static files with compression and caching
)

WhiteNoise automatically:

  • ✅ Compresses static files (gzip, brotli)
  • ✅ Sets cache headers (Cache-Control: max-age=31536000)
  • ✅ Serves with optimal performance

2. Gunicorn Workers

# Calculate optimal workers: (2 * CPU cores) + 1
gunicorn --workers 9 --bind 0.0.0.0:8000 core.wsgi:application

# With threading
gunicorn --workers 4 --threads 2 --bind 0.0.0.0:8000 core.wsgi:application

3. CDN Integration (Optional)

For global deployments, use a CDN:

# Configure CDN URL
STATIC_URL = "https://cdn.yourdomain.com/static/"

Upload ZIP contents to CDN:

# Extract ZIP
unzip static/nextjs_admin.zip -d /tmp/nextjs_admin

# Upload to S3 + CloudFront
aws s3 sync /tmp/nextjs_admin s3://yourbucket/static/nextjs_admin/ \
--cache-control "max-age=31536000"

# Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id XXXXX \
--paths "/static/nextjs_admin/*"

Monitoring and Logging

Health Check Endpoint

api/urls.py
from django.http import JsonResponse

def health_check(request):
return JsonResponse({
"status": "healthy",
"nextjs_admin": has_nextjs_admin(),
})

urlpatterns = [
path("health/", health_check),
# ...
]

Logging Configuration

api/config.py
config = DjangoConfig(
# ...

logging={
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
"file": {
"class": "logging.FileHandler",
"filename": "/var/log/myapp/django.log",
},
},
"loggers": {
"django_cfg.apps.frontend": {
"handlers": ["console", "file"],
"level": "INFO",
},
},
},
)

Monitor ZIP Extraction

# Docker logs
docker logs -f myapp | grep "Extracting"

# Output:
# INFO: Extracting nextjs_admin.zip to /app/static/nextjs_admin/...
# INFO: Successfully extracted nextjs_admin.zip (7.2MB in 95ms)

Troubleshooting Production

ZIP Not Extracting

Check 1: Does ZIP file exist?

docker exec myapp ls -lh /app/static/nextjs_admin.zip

Check 2: Permissions

docker exec myapp ls -ld /app/static/
# Should be writable by app user

Check 3: Disk space

docker exec myapp df -h

404 on Admin Page

Check 1: URL configuration

# Verify static_url in config
nextjs_admin=NextJsAdminConfig(
static_url="/cfg/admin/", # Check this matches your URLs
)

Check 2: URL patterns

# List URLs
docker exec myapp python manage.py show_urls | grep admin

Slow First Request

This is expected (ZIP extraction). Optimize with:

  1. Pre-extract in Docker build (not recommended, increases image size)
  2. Use persistent volumes (extraction persists across restarts)
  3. Accept one-time cost (~100ms, only first request)

CI/CD Integration

GitHub Actions Example

.github/workflows/deploy.yml
name: Deploy

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'

- name: Install dependencies
run: pip install -r requirements.txt

- name: Generate API and build Next.js
run: |
python manage.py generate_clients --typescript
ls -lh static/nextjs_admin.zip

- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .

- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push myapp:${{ github.sha }}

- name: Deploy to production
run: |
# Deploy command (depends on your infrastructure)
kubectl set image deployment/myapp web=myapp:${{ github.sha }}

Next Steps

Production Checklist

Use our production checklist to ensure nothing is missed.