django → aws · 2026 deployment & migration playbook

Migrate a Django app to AWS — the target architecture, the cutover, and who runs it for you.

The senior-engineer playbook for deploying Django on AWS and moving a production app there: the target architecture (gunicorn/uvicorn on ECS Fargate or App Runner, RDS/Aurora PostgreSQL, ElastiCache for Celery and cache, S3 + CloudFront for static and media via django-storages, an ALB out front), how to Dockerize the app, settings and secrets into Secrets Manager / SSM, Celery workers, running migrations and collectstatic in CI/CD, the database cutover with DMS, and the gotchas that bite Django teams. A MAP-funded AWS partner can run the whole thing — often at little-to-no cost to you.

project length
3–8 weeks
cutover downtime
0–15 min
compute target
Fargate / App Runner
cost to you (MAP)
$0–low
TL;DR
  • A Django app maps cleanly onto AWS: the WSGI/ASGI server (gunicorn for sync, uvicorn/gunicorn-with-uvicorn-workers for async) runs as containers on ECS Fargate (full control) or App Runner (closest to a PaaS), behind an Application Load Balancer; Postgres becomes RDS or Aurora PostgreSQL; Celery's broker and your cache become ElastiCache (Redis OSS/Valkey); static and media files move to S3 + CloudFront via django-storages.
  • The work that is genuinely Django-specific: writing the Dockerfile (gunicorn entrypoint, a non-root user, WhiteNoise or S3 for static), splitting settings into environment-driven config with secrets pulled from AWS Secrets Manager / SSM Parameter Store, running `migrate` and `collectstatic` as deploy steps in CI/CD (not at container boot), and standing up Celery workers + beat as their own ECS services. None of it touches your models or business logic.
  • The risky moment is the database cutover, and it is solvable: AWS DMS keeps the new RDS/Aurora Postgres continuously synced (CDC) with your current database while you test, then a short maintenance/DNS switch (typically 0–15 min) flips traffic. CloudRoute routes you to a vetted AWS partner who runs it end-to-end — and when the workload qualifies for the AWS Migration Acceleration Program (MAP), AWS funds the migration and the customer pays little-to-nothing.
the trigger

IWhy teams move a Django app to AWS

Django apps arrive at AWS from two directions: teams outgrowing a PaaS (Heroku, Render, DigitalOcean App Platform) where the bill and the platform limits have stopped making sense, and teams on a single VPS or on-prem box that need real high-availability, managed Postgres, and room to scale. The destination is the same; the reasons rhyme.

The first reason is cost at scale. A PaaS charges a flat premium for abstracting the infrastructure — fine for a v1, expensive once you run multiple web processes, Celery workers, a managed Postgres plan, and a managed Redis add-on around the clock. On AWS you buy the underlying services (Fargate or EC2, RDS/Aurora, ElastiCache) at list price, then bend the bill down with a Compute Savings Plan and right-sizing. For a mid-size Django app the move commonly lands 30–60% lower, and the gap widens as you add workers.

The second reason is the architecture Django apps actually need. A real deployment is not one process — it is a WSGI/ASGI web tier, a Celery worker tier, usually a Celery beat scheduler, Postgres, a Redis broker/cache, and somewhere to serve static and media. A PaaS can do all of this, but coordinating it — private networking between web and database, connection pooling, scaling each tier independently, blue/green deploys — is exactly where its abstraction starts to fight you. ECS on Fargate models each tier as its own service with its own scaling policy, inside your VPC.

The third reason is the surrounding requirements: VPC isolation, IAM, private subnets for the database, SOC 2 / HIPAA controls, multi-AZ failover, and integration with the rest of an AWS estate (S3, SQS, SES, CloudFront, KMS). These are first-class on AWS and bolted-on or absent elsewhere — frequently the deciding factor for a company in security review or selling into the enterprise, ahead of cost.

The honest counterpoint: AWS asks you to own more than a PaaS does — a landing zone, networking, IAM, container images, deploy pipelines, observability. That operational surface is precisely why most teams do not run this migration alone. The rest of this page is the architecture and the plan; the CloudRoute angle is that a vetted AWS partner runs it for you, and AWS often funds it through MAP.

the margin math

A mid-size Django app commonly runs $900–$2,500/month on a PaaS once you count multiple web processes, two or three Celery workers, a managed Postgres plan, managed Redis, and log/metrics add-ons. The equivalent on AWS — Fargate (web + workers) + RDS/Aurora + ElastiCache + S3/CloudFront + CloudWatch, with a Compute Savings Plan — commonly lands at $450–$1,300/month. That recovered margin is what pays for the migration (and with MAP, AWS pays for it instead).

where things land

IIThe target architecture: a Django app on AWS, tier by tier

There is no single "AWS version of a PaaS dyno." There are two sensible compute targets, and the right one depends on how much control you want versus how close to a push-to-deploy experience you want to stay. Everything else — database, broker, cache, static, media — maps almost one-to-one.

For compute, the two realistic targets are AWS App Runner and Amazon ECS on Fargate. App Runner is the closest thing AWS has to a PaaS: point it at a container image (or source repo) and it builds, deploys, serves HTTPS, and autoscales on request volume — the fastest path for a single straightforward web service. ECS on Fargate is the workhorse: serverless containers with full control over the VPC, subnets, security groups, task sizing, per-service autoscaling, an Application Load Balancer, and clean separation of the web tier from Celery workers and beat. Teams migrating a real Django app almost always want Fargate, because Celery and independent tier scaling are exactly what App Runner abstracts away. (EKS is overkill for a single Django app unless you already run Kubernetes.)

The web tier runs your project under a production WSGI or ASGI server. For a conventional synchronous app that means gunicorn with a sensible worker count (a common starting point is `2 × vCPU + 1`, tuned by load testing). For an app using async views, Django Channels, or ASGI, you run uvicorn — typically `gunicorn` with `uvicorn.workers.UvicornWorker` — so async views and WebSockets work as intended. Either way the container exposes a port, the ALB terminates TLS (via an ACM certificate) and routes to the ECS service, and health checks hit a lightweight endpoint.

For the database, Postgres becomes Amazon RDS for PostgreSQL (the like-for-like managed Postgres) or Aurora PostgreSQL (AWS's cloud-native engine — a little more per hour, but faster failover, autoscaling storage, up to 15 read replicas, and Serverless v2 capacity scaling). Because Django's ORM speaks PostgreSQL either way, your models, migrations, and queries do not change — you point `DATABASES` at the new endpoint. Put the database in private subnets, reachable only from the application security group.

Celery is the piece a generic "deploy a web app" guide forgets. It needs a broker and usually a result backend; on AWS that is Amazon ElastiCache for Redis (OSS/Valkey), and the same cluster doubles as Django's cache backend. Each worker pool runs as its own ECS Fargate service (`celery -A proj worker`), and Celery beat (the periodic scheduler) runs as a single-instance service (`celery -A proj beat`) so schedules don't double-fire. Some teams use Amazon SQS as the broker instead of Redis to drop a stateful component; Celery supports both, and the choice is a real architectural decision a partner makes with you.

Static and media files are the other Django-specific concern. Static assets (CSS/JS/admin) are gathered by `collectstatic` and served from Amazon S3 behind Amazon CloudFront via django-storages (`STORAGES`/`STATICFILES_STORAGE`); small apps can instead serve static from the container with WhiteNoise and skip S3 for static. User-uploaded media must go to S3 via django-storages regardless — container filesystems are ephemeral and there are multiple tasks, so writing uploads to local disk silently breaks the moment a second container starts. CloudFront fronts both with a CDN and TLS. Outbound email (a Django `EMAIL_BACKEND`) moves to Amazon SES or stays with your provider billed directly.

App Runner vs Fargate — pick based on Celery and control

App Runner for a single stateless Django web service when you want push-to-deploy simplicity and have little or no background work (or you run it elsewhere). ECS on Fargate the moment you have Celery workers + beat, need VPC networking control, want to scale the web and worker tiers independently, or need private connectivity to RDS/ElastiCache — the common landing spot for a production Django app. Skip EKS unless you already run Kubernetes.

the lookup table

IIIDjango component → AWS service (the principles behind the table)

The full row-by-row mapping — every Django building block, its AWS service, the cost direction, and the effort — lives in the comparison table further down. Two principles make reading that table safe.

First, anything that speaks a standard protocol moves with a config change and no application rewrite: PostgreSQL (your `DATABASES` setting → RDS/Aurora endpoint), Redis (Celery broker + Django cache → ElastiCache endpoint), and S3 (media via django-storages). Your models, tasks, and views are untouched — you are changing connection strings and storage backends, not logic.

Second, anything PaaS- or host-proprietary needs a one-time translation into an AWS-native primitive: the process model (web/worker/beat) → separate ECS services, environment config and secrets → Secrets Manager + SSM Parameter Store, the release/deploy hook (where `migrate` and `collectstatic` run) → a CI/CD step, the build → a Dockerfile + ECR, and TLS/custom domains → Route 53 + ACM + CloudFront. Done once and documented, that translation is the work a migration partner does — the "Medium/High effort" rows in the table.

the hands-on part

IVDockerizing Django: gunicorn/uvicorn, settings, and secrets

The single most hands-on task in a Django→AWS move is turning the app into a production container image and moving its configuration off the host and into AWS-managed config. It is mechanical, but it is where teams without container experience slow down — so here is exactly what it involves.

The Dockerfile is short and standard: a slim Python base (e.g. `python:3.12-slim`), OS build deps only if a wheel needs compiling, copy and install `requirements.txt` (or a Poetry/uv lockfile) into a clean layer, copy the project, switch to a non-root user, and set the start command to your WSGI/ASGI server — `gunicorn proj.wsgi:application --bind 0.0.0.0:8000 --workers N` for sync, or `gunicorn proj.asgi:application -k uvicorn.workers.UvicornWorker` for async. The same image serves every tier: the web service runs gunicorn/uvicorn, the worker service overrides the command to `celery -A proj worker`, and beat overrides it to `celery -A proj beat`. Build once, run three ways — that is the ECS pattern.

Settings are the other translation, and Django's flat `settings.py` is the first thing to refactor. Drive everything from environment variables (django-environ, or `os.environ` with `python-dotenv` for local dev): `DEBUG`, `ALLOWED_HOSTS`, `DATABASE_URL`, `CELERY_BROKER_URL`, `CACHE_URL`, `SECRET_KEY`, API keys. On AWS those split by sensitivity — real secrets (`SECRET_KEY`, database credentials, API keys, signing keys) in AWS Secrets Manager; non-sensitive config (feature flags, public URLs, log level, bucket name) in SSM Parameter Store. ECS injects both as environment variables via the task definition's `secrets` and `environment` blocks, so the container sees them exactly as it would locally and your code reads `os.environ` unchanged. The migration step is a one-time inventory, a sort into secret-vs-config, and a load into Secrets Manager / Parameter Store — which a partner scripts so nothing is missed or baked into an image layer.

Two production-settings details to fix while you are in there. Set `DEBUG=False` and `ALLOWED_HOSTS` to your real domain plus the ALB/health-check host (a misconfigured `ALLOWED_HOSTS` is the most common cause of a 400 on AWS — see the gotchas). And configure the database connection deliberately: enforce SSL to RDS/Aurora and use `CONN_MAX_AGE` carefully — persistent connections across many autoscaled tasks can exhaust Postgres connections, which is why RDS Proxy or PgBouncer belongs in the design, not as an afterthought.

the deploy contract

VMigrations and collectstatic in CI/CD — not at container boot

On a single-server or single-dyno setup you can get away with running `migrate` and `collectstatic` when the app starts. On AWS, with multiple tasks starting in parallel behind an ALB, that pattern causes race conditions and flaky deploys. The fix is to make them explicit, ordered deploy steps.

Run `python manage.py collectstatic --noinput` at build time (or in the CI pipeline) so the image — or the S3 bucket, via django-storages — already has the gathered static assets before any container serves traffic. Baking collection into the build keeps the runtime container fast to start and means every task serves identical assets. If you serve static from S3/CloudFront, the pipeline runs `collectstatic` with the S3 backend so the files land in the bucket; with WhiteNoise they are collected into the image.

Run database migrations as a single one-off task per deploy — never in the web container's entrypoint, where N parallel tasks would each try to apply the same migration. The clean pattern on ECS is a dedicated `migrate` step in the pipeline: the CI job runs `aws ecs run-task` with the same image and the command overridden to `python manage.py migrate --noinput`, waits for it to succeed, and only then updates the web and worker services. CodePipeline + CodeBuild can orchestrate this, or you keep GitHub Actions running build → push to ECR → migrate task → update services. Either way the contract is: migrate first, then roll the services.

This ordering is also what makes zero-downtime deploys safe. Use backward-compatible migrations (add columns/tables before the code that needs them; remove only after the old code is gone) so the brief window where old and new tasks coexist never sees a schema the running code cannot handle. ECS rolling updates (or blue/green via CodeDeploy) then replace tasks gradually behind the ALB with no downtime — a discipline that pays off well beyond the migration.

the deploy sequence that avoids 90% of Django-on-ECS pain

1. Build image, run collectstatic (to S3 or into the image). 2. Push image to ECR. 3. Run a one-off migrate task; wait for success. 4. Update the web service (rolling or blue/green). 5. Update the Celery worker + beat services. Migrations are backward-compatible, so steps 4–5 are safe while old tasks drain.

the background tier

VICelery workers, beat, and the broker on AWS

Celery is where Django deployments differ most from a plain web app, and where a naive lift-and-shift goes wrong. The web tier and the background tier have different scaling, failure, and concurrency characteristics, so they get modeled as separate services.

The broker is the first decision. The simplest choice is Amazon ElastiCache for Redis (OSS/Valkey) as both the Celery broker and result backend, and the same cluster as Django's cache — one managed component covering three jobs, with a connection-string change from your current Redis. The alternative is Amazon SQS as the broker, which removes a stateful service and scales effortlessly, at the cost of some Redis-specific Celery features (and SQS is not a result backend, so you pair it with another store). For most teams ElastiCache is the like-for-like move; SQS is worth it when you want managed, near-infinite queue capacity and minimal ops.

Each worker pool is its own ECS Fargate service running `celery -A proj worker -l info`, with concurrency and autoscaling tuned to the work — CPU-bound tasks want lower concurrency and scale on CPU; IO-bound tasks tolerate higher concurrency. Splitting queues by workload (e.g. a `default` and a `heavy` queue on separate services) keeps a slow report job from starving fast user-facing tasks and lets each scale independently. Celery beat runs as a separate single-replica service so scheduled tasks fire exactly once — running two beat instances double-schedules everything, a classic post-migration bug.

Observability for the worker tier matters as much as for web. Ship Celery logs to CloudWatch, alarm on queue depth (Redis list length or SQS `ApproximateNumberOfMessagesVisible`) and on task failure rates, and set task time limits and retries so a stuck task can't pin a worker forever. EventBridge Scheduler can replace Celery beat for simple cron jobs if you prefer AWS-native scheduling, though most teams keep beat through the migration and revisit later.

the risky 15 minutes

VIIThe database cutover: DMS, DNS, and a short downtime window

Everything else — containers, the landing zone, S3/CloudFront, Celery services — can be built and tested without touching production. The database cutover is the one moment that touches live data, where a plan earns its keep. Done right, user-visible downtime is zero to fifteen minutes.

The core technique is to keep the new RDS/Aurora Postgres continuously in sync with your current database while you test, so that at cutover you switch to an already-current copy rather than copying a cold database under time pressure. AWS Database Migration Service (DMS) does exactly this: a full load into RDS/Aurora, then change data capture (CDC) that streams every subsequent insert/update/delete in near-real time. Because both ends are PostgreSQL this is a homogeneous migration — no Schema Conversion Tool needed; Django's schema moves as-is (a `pg_dump --schema-only` restore, or let your migrations build it and DMS replicate the data). Very small databases can skip DMS for a single `pg_dump | pg_restore` inside the window; DMS earns its place once a cold dump/restore would blow past an acceptable window.

With CDC running and the new stack smoke-tested against the synced database, the cutover is short and scripted: enable a maintenance page → let DMS drain the last few seconds → pause Celery beat and let workers finish in-flight tasks → flip `DATABASES` (and `CELERY_BROKER_URL`/`CACHE_URL`) to the AWS endpoints and switch traffic to the new ALB/App Runner service → update Route 53 (TTL lowered ahead of time) → verify writes land in RDS → exit maintenance. Because the data was already synced, the window is dominated by DNS propagation and verification, not data transfer — commonly 0–15 minutes during a low-traffic period.

The rollback plan is non-negotiable and simple: keep the old environment running until you are confident, and if something is wrong, switch the database connection and DNS back. The clean window is before RDS has accepted production writes the old database hasn't seen — once committed, rolling back means reconciling those writes — so most teams keep it short (minutes to hours) and watch closely. Lower DNS TTL to 60 seconds a day ahead so both the switch and rollback are fast. One Django-specific note: confirm the new database's sequence values are correct after a data-only load (DMS does not always advance sequences), or the first inserts collide on primary keys.

why DMS over a cold dump

A cold pg_dump | pg_restore makes your downtime window = export + transfer + import of the whole database: 20–60 minutes for 5GB, unacceptable at 50GB+. DMS with CDC moves the bulk load before the window and only drains the last few seconds during it — so downtime stays in single-digit minutes regardless of database size. Validate row counts and re-sync Postgres sequences before you exit maintenance.

the runbook

VIIIThe step-by-step migration (assess → land → cut over → optimize)

Here is the end-to-end sequence a partner runs, mapped to the AWS MAP phases (Assess → Mobilize → Migrate). For a typical Django app this is a 3–8 week project — most of it parallelizable, none of it touching production until the final cutover.

  • 1 — Assess (days 1–5) — Inventory the web process model, every Celery worker + beat schedule, the database size and connection pattern, static/media storage, and every setting and secret. Decide App Runner vs Fargate, RDS vs Aurora, and Redis vs SQS for the broker; produce the target architecture and a cost before/after model. Under MAP this Assess/TCO phase is typically free.
  • 2 — Build the landing zone — Stand up the AWS account structure, VPC, public/private subnets, security groups, IAM roles, ECR, and Secrets Manager / Parameter Store. (Reuses the AWS Landing Zone and AWS DevOps foundations in the DevOps cluster.)
  • 3 — Containerize — Write the Dockerfile (gunicorn/uvicorn entrypoint, non-root user), parameterize settings via environment variables, and confirm the image builds and boots locally and in CI — web, worker, and beat all from the one image.
  • 4 — Provision data, cache, and storage — Create the RDS/Aurora instance (private subnets, SSL, RDS Proxy for pooling) and the ElastiCache cluster in the VPC; create the S3 buckets for static + media and the CloudFront distribution; wire django-storages.
  • 5 — Start replication — Run a DMS full load from the current Postgres into RDS/Aurora, then switch to CDC so the new database tracks production while you test. Validate row counts, known records, and sequence values.
  • 6 — Deploy + smoke-test — Run collectstatic and a one-off migrate task, deploy the web + Celery services against the synced database (no public traffic yet), and run the full test suite + manual smoke tests including background tasks, scheduled jobs, file uploads, and the admin.
  • 7 — Cut over — Maintenance page → drain CDC → pause beat → flip DATABASES + broker/cache → update Route 53 (low TTL pre-set) → re-sync sequences, verify writes land in RDS → exit maintenance. Target window: 0–15 minutes.
  • 8 — Optimize + decommission — Apply a Compute Savings Plan, right-size Fargate tasks and the RDS/Aurora instance, set per-tier autoscaling, confirm CloudWatch alarms, backups, and CloudFront caching. Keep the old environment as rollback for a short window, then shut it down — that shutdown is when the savings become real.
component-by-component

Django component → AWS service (cost direction + effort)

The definitive lookup: every Django building block, where it lands on AWS, the rough cost direction, and the engineering effort. "Trivial" rows are config or backend swaps; "Medium/High" rows are the real work — and the work a partner does for you.

Django componentAWS serviceWhat changesCost directionEffort
WSGI web (gunicorn)ECS/Fargate + ALB, or App RunnerContainerize; ALB terminates TLSDown 30–60%Medium
ASGI / async / Channels (uvicorn)ECS/Fargate + ALB (uvicorn workers)uvicorn worker class; ALB/WebSocket configDown 30–60%Medium
PostgreSQL databaseRDS for PostgreSQL / Aurora PostgreSQLDATABASES endpoint + cutover via DMSDown 30–55%High (cutover)
Celery broker + result backendElastiCache (Redis/Valkey) or Amazon SQSCELERY_BROKER_URL; or SQS transportDown 40–70%Medium
Celery workersECS/Fargate service (per queue)Worker process → its own serviceDown 30–60%Medium
Celery beat (scheduler)ECS single-replica service, or EventBridge SchedulerRun exactly one beat; or AWS cronNear $0Low
Django cache frameworkElastiCache (Redis/Valkey)CACHES backend → ElastiCache endpointDown 40–70%Trivial
Static files (collectstatic)S3 + CloudFront (django-storages) or WhiteNoiseSTORAGES backend; collectstatic in CIDown / neutralLow
User media uploadsAmazon S3 (django-storages)DEFAULT_FILE_STORAGE → S3DownLow
settings.py configSSM Parameter StoreRead from env injected by ECSNegligibleLow
SECRET_KEY + DB creds + API keysAWS Secrets ManagerInject as task secrets; never in imageNegligibleLow
Dependencies + build (pip/Poetry)Dockerfile + ECRExplicit container buildNegligibleMedium
Email backend (SMTP)Amazon SES (or provider direct)EMAIL_BACKEND / SMTP configDownLow
Custom domain + TLSRoute 53 + ACM + CloudFrontDNS + free cert on ALB/CloudFrontDown (free certs)Low
Cost directions are representative 2026 ranges and depend on right-sizing and commitments (Savings Plans, committed DB capacity). The high-effort rows — the Postgres cutover, containerization, the Celery tier — are exactly what a MAP-funded partner handles end-to-end.
want this run for you — and funded?
Get matched with an AWS partner who runs your Django migration (often MAP-funded)
Start in 3 minutes →
a recent match

A Django→AWS migration CloudRoute routed — anonymized

inquiry · series-a b2b saas (django + celery), Toronto
Series-A B2B SaaS on a Django 4.2 monolith: gunicorn web, three Celery worker pools + beat, managed Postgres (~80GB), managed Redis, and user uploads on a PaaS object store. PaaS bill ~$2,200/month and climbing; hitting Postgres connection limits at peak and unable to scale workers independently of web.

Situation: Margin pressure ahead of a Series-B raise plus a SOC 2 commitment that required VPC isolation and a private database — neither comfortable on the PaaS. The three-person team had no AWS or container experience and could not afford a multi-month DIY migration or a risky big-bang database cutover, especially with 80GB of Postgres and a large media bucket to move.

What CloudRoute did: Routed within 24 hours to an AWS Advanced-tier partner with a Django + Celery track record, who ran the MAP Assess phase (free) and filed the work as a MAP engagement. Target: ECS on Fargate (gunicorn web behind an ALB; two Celery worker services split by queue; a single-replica beat), Aurora PostgreSQL with RDS Proxy for pooling, ElastiCache for the Celery broker + Django cache, S3 + CloudFront for static and media via django-storages, settings parameterized with secrets in Secrets Manager + SSM. CI/CD via GitHub Actions: build → ECR → one-off migrate task → rolling ECS update, with collectstatic at build time. Postgres moved with AWS DMS (full load + CDC); the media bucket synced with S3 DataSync ahead of cutover.

Outcome: Cutover ran in a Saturday-night window with ~13 minutes of write-downtime — DMS had Aurora fully in sync, so the switch was DNS + sequence re-sync + verification, not data transfer. Steady-state AWS bill landed at ~$1,050/month (a ~52% cut); the connection ceiling was gone behind RDS Proxy, and the worker tiers now scale independently of web. Project ran ~6 weeks. Because the workload qualified for MAP, AWS funded the assessment and credited the migration cost — out-of-pocket migration cost was effectively $0, and CloudRoute's commission was paid by the partner from MAP funding.

project length: ~6 weeks · cutover downtime: ~13 min · monthly spend: $2,200 → $1,050 (−52%) · migration cost to customer: ~$0 (MAP-funded)

faq

Common questions

What is the best way to deploy Django on AWS in 2026?
For a production Django app, the standard is to run gunicorn (sync) or uvicorn (async/ASGI) as a container on Amazon ECS with Fargate, behind an Application Load Balancer, with PostgreSQL on RDS or Aurora, ElastiCache (Redis/Valkey) for the Celery broker and Django cache, and static + media on S3 + CloudFront via django-storages. AWS App Runner is a simpler alternative when you have a single web service and little or no background work. ECS/Fargate is preferred for most real apps because it lets you run and scale Celery workers and beat as their own services inside your VPC.
How long does a Django to AWS migration take?
For a typical Django app — a web tier, a few Celery workers and beat, Postgres, Redis, and static/media storage — a partner-run migration is usually 3–8 weeks end-to-end. Most of that is building and testing the landing zone, the container image, the Celery services, and a synced database in parallel, without touching production; only the final database cutover (a single short maintenance window) touches live traffic.
How do I handle Celery workers and scheduled tasks on AWS?
Run each Celery worker pool as its own ECS Fargate service (the same image, with the command overridden to `celery -A proj worker`), and run Celery beat as a single-replica service so periodic tasks fire exactly once. The broker and result backend are Amazon ElastiCache for Redis, or Amazon SQS if you prefer a fully managed queue with no stateful component. Ship worker logs to CloudWatch, alarm on queue depth, and set task time limits and retries. EventBridge Scheduler can replace beat for simple cron jobs if you want AWS-native scheduling.
Where should migrations and collectstatic run?
Not at container boot — with multiple tasks starting in parallel behind an ALB, that causes race conditions. Run `collectstatic` at build time (into the image, or into S3 via django-storages) so assets are ready before any container serves traffic. Run `migrate` as a single one-off ECS task per deploy (e.g. `aws ecs run-task` with the command overridden), wait for it to succeed, and only then roll the web and worker services. Keep migrations backward-compatible so old and new tasks can coexist during a rolling deploy — that is what makes zero-downtime deploys safe.
How do I manage Django settings and secrets on AWS?
Refactor settings.py to read from environment variables, then split values by sensitivity. Real secrets — SECRET_KEY, database credentials, API keys, signing keys — go in AWS Secrets Manager; non-sensitive config like feature flags, public URLs, and the S3 bucket name go in SSM Parameter Store. ECS injects both into the task as environment variables via the task definition, so your code reads os.environ unchanged and nothing sensitive is ever baked into the container image. A partner scripts the one-time export-and-load so no setting is missed or leaked.
How much downtime is there at the database cutover?
Typically 0–15 minutes of write-downtime, run during a low-traffic period. AWS Database Migration Service (DMS) bulk-loads your PostgreSQL data into RDS/Aurora ahead of time, then streams ongoing changes via change data capture. At cutover you drain the last few seconds, flip the DATABASES endpoint and broker/cache URLs, update DNS, and re-sync Postgres sequences — so the window is dominated by DNS propagation and verification, not copying data, regardless of database size. Keep the old environment running as a fast rollback.
Do I need to change my Django code or models to run on AWS?
Largely no. Postgres → RDS/Aurora is a homogeneous PostgreSQL move, so your models, migrations, ORM queries, and the Django admin are unchanged — you point DATABASES at the new endpoint. Celery and the cache keep working with connection-string changes to ElastiCache (or an SQS transport). The code you do touch is configuration, not logic: the Dockerfile, environment-driven settings, the django-storages backends for static/media, and ALLOWED_HOSTS / CSRF_TRUSTED_ORIGINS / SECURE_PROXY_SSL_HEADER for running behind an ALB.
What does AWS MAP funding mean for the cost of the migration?
The AWS Migration Acceleration Program (MAP) is how the migration itself can cost little to nothing. For qualifying migrations — generally those with a meaningful projected AWS spend afterward — AWS funds the Assess phase (TCO/readiness, typically free) and credits a large share of the migration/modernization cost, paying the partner through MAP rather than you. When a workload does not qualify, CloudRoute still routes you to a vetted partner who runs the cutover at a fixed scope, so you are not improvising the riskiest 15 minutes of your year.

Move your Django app to AWS — run by a partner, often funded by AWS.

CloudRoute routes you to a vetted AWS partner who plans and runs the whole Django→AWS migration — gunicorn/uvicorn on ECS Fargate or App Runner, RDS/Aurora Postgres, ElastiCache for Celery, S3 + CloudFront for static and media, the DMS cutover, and the cost optimization. Qualifying migrations are MAP-funded, so you capture the ongoing savings without paying the usual migration bill.

matched within< 24h
typical cost cut30–60%
migration cost (MAP)$0–low
Migrate a Django App to AWS — Architecture & Cutover (2026) · CloudRoute