Skip to main content
Tutorial

Deploy Django on AWS EC2 — production monitoring next steps

The Reflex Team10 min12 May 2026

Launch day on AWS EC2 is a marathon: VPC and security groups, Ubuntu AMI, Gunicorn behind nginx, Celery workers on Redis or SQS, RDS PostgreSQL, static files to S3, secrets in Parameter Store. Django runs. The ELB health check goes green. Then production reality arrives — memory creep on Gunicorn workers, Celery queue depth climbing, RDS connection exhaustion, and disk full on the root volume from gunicorn logs nobody rotated.

Deploying Django on AWS is well trodden. Monitoring the Linux layer under your app is what keeps on-call sane after the launch blog post is published.

Architecture recap

Typical production layout:

  • EC2 — Gunicorn serving Django WSGI/ASGI via nginx
  • RDS — PostgreSQL or MySQL with connection limits you will hit under load
  • ElastiCache or EC2 Redis — Celery broker and Django cache
  • S3 + CloudFront — static and media if configured
  • ALB — TLS termination and target group health checks

The ALB checks /health/ — make it honest:

# urls.py — verify database connectivity
def health(request):
    from django.db import connection
    connection.ensure_connection()
    return JsonResponse({"status": "ok"})

TCP-only health checks lie when Gunicorn is wedged.

Gunicorn production config

On EC2, Gunicorn worker count follows CPU — often (2 * cores) + 1, but memory is the real limit. Each Django worker can consume 150–400MB depending on middleware and ORM caching.

Example systemd unit /etc/systemd/system/gunicorn.service:

[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/venv/bin/gunicorn \
    --workers 3 \
    --timeout 120 \
    --bind unix:/run/gunicorn.sock \
    myproject.wsgi:application
Restart=always

Tune workers after measuring RSS under load — same math as PHP-FPM: total worker memory must fit in instance RAM minus RDS proxy, Redis, and OS. The OOM analyser helps estimate headroom on your instance type.

Reload after deploy:

sudo systemctl reload gunicorn

Stale workers serving old code is a common post-deploy bug.

Celery workers

Supervisor or systemd manages Celery:

celery -A myproject worker -l info --concurrency=4
celery -A myproject beat -l info   # if scheduled tasks

Monitor:

  • Queue depth in Redis or SQS metrics
  • Worker process count — crash loops from bad tasks
  • Task failure rate in Flower or CloudWatch custom metrics

A silent Celery outage means emails, reports, and webhooks stop — HTTP still returns 200.

nginx and disk hygiene

Logrotate for nginx and gunicorn:

/var/log/gunicorn/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
}

CloudWatch agent can ship logs, but disk full on EC2 still kills the instance before you search Logs Insights.

RDS and connections

Django CONN_MAX_AGE persistent connections help latency but exhaust small RDS instances when Gunicorn workers multiply:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "CONN_MAX_AGE": 60,
        ...
    }
}

Watch DatabaseConnections in CloudWatch. PgBouncer on EC2 between app and RDS is common at scale.

Deploy checklist beyond CI green

  1. Run migrations with backup snapshot first
  2. Collect static files to S3 if applicable
  3. Reload Gunicorn and restart Celery workers
  4. Run smoke tests against ALB URL — admin login, critical API, Celery ping task
  5. Health-gate — hold traffic until error rates stable; see health-gated deployments

Rollback plan: previous AMI or codedeploy revision, database migration reversibility documented before deploy day.

Use IMDSv2 on EC2, restrict security groups to ALB-only on Gunicorn ports, and rotate Django SECRET_KEY only with session invalidation planned — monitoring cannot fix security misconfigurations but should alert on disk and process anomalies caused by cryptomining after a breach.

ASGI and Channels

If you run Django Channels on Daphne or Uvicorn, add process monitoring for those units separately from Gunicorn — a green WSGI health check hides dead websocket workers.

What AWS native monitoring misses

CloudWatch on EC2 gives CPU, network, status checks — not Gunicorn worker memory leaks, Celery backlog age, or deploy correlation. ALB 5xx counts help but lag behind process-level failure.

You still need process and queue visibility plus optional auto-repair for known mechanical failures — restart stuck workers, disk pressure from logs — before paging humans. Alert fatigue hits Django teams the same as Laravel when every CPU blip pages Slack.

Set CloudWatch alarms on StatusCheckFailed and disk — but pair with reflexd-level FPM-equivalent process metrics on Gunicorn and Celery, not instead of them.

Schedule a recurring calendar invite for logrotate and Celery queue depth review — boring maintenance prevents exciting outages.

Django on AWS is never "finished" at launch — production begins when monitoring and repair paths are wired, not when the ELB goes green.

How Reflex helps Django on EC2

Reflex is not Django-specific in application code — reflexd monitors the Linux layer: process memory, disk, systemd services, nginx, and deploy markers from your CI. Brain playbooks restart Gunicorn or Celery workers when policy allows, with audit trails.

Django monitoring with Reflex documents Django-focused signals and setup. Pair with PagerDuty vs auto-fix if your stack pages without repairing.

Multi-client agencies running Django and Laravel on AWS can unify fleet view — Reflex for agencies. View pricing. How the Brain repair cycle works. Compare Reflex vs Datadog for observability cost on EC2 fleets.

Ready to stop firefighting your servers?

Try Reflex free for 14 days.