Move a Django Site to a New Server
Migrate a production Django site to a new server or provider with minimal downtime: rebuild the stack from code, copy media with rsync, move PostgreSQL with dump and restore or replication, lower DNS TTL, cut over, and keep the old server as a fallback.
Problem statement
The server is old, the OS is out of support, the provider is too expensive, or the app has outgrown the machine. Moving a live Django site means moving code, configuration, secrets, the database, uploaded media, certificates, scheduled jobs, and DNS, without losing data written during the move and without a long outage. Doing it ad hoc on a Friday evening is how sites lose a day of orders. A rehearsed plan with a short write freeze makes it routine.
- Lower the DNS TTL to 300 seconds at least a day before the move.
- Build the new server from your provisioning scripts (Ansible, cloud-init, or Compose), not by copying the old one.
- Copy media with
rsyncin advance; copy the database once to rehearse and measure. - At cutover: enable maintenance mode, final
rsync,pg_dump/pg_restore(or promote a replica), runmigrate --checkand smoke tests on the new server. - Switch DNS (or a floating IP), confirm traffic arrives, then disable maintenance mode.
- Keep the old server read-only for a few days, then decommission and wipe it.
Step-by-step solution
1. Inventory what lives on the old server
Write down everything the app depends on:
- Code version, Python version, system packages (
libpq, image libraries). - Environment file and secrets; Nginx, Gunicorn, Celery, and timer units.
- PostgreSQL version, extensions, roles, database size.
MEDIA_ROOTlocation and size; any files outside it the app writes.- Cron jobs and systemd timers; certificates and renewal hooks; firewall rules.
Anything not in your provisioning scripts is a gap to close before the move.
2. Build the new server from code
Provision it with the same scripts you would use for a fresh environment, install the target OS and PostgreSQL version, deploy the current release, and configure Nginx with a temporary hostname such as new.example.com to test.
3. Pre-copy media and rehearse the database
# media, repeatable; only changes are sent on later runs
rsync -aHAX --delete --info=progress2 old:/srv/app/shared/media/ /srv/app/shared/media/
# database rehearsal
ssh old 'pg_dump -Fc -d app' > /tmp/app.dump
pg_restore --no-owner --role=app -d app -j 4 /tmp/app.dump
Time the rehearsal: the dump and restore duration is roughly your downtime. For large databases where that is too long, set up logical replication from old to new ahead of time so cutover is just stopping writes and promoting; see Migrate a Django Database With Logical Replication.
4. Cut over
- Enable maintenance mode on the old server so no new writes happen (or stop Gunicorn and Celery and serve a static page from Nginx).
- Run the final
rsync(fast, only changes) and the final database copy. - On the new server:
python manage.py migrate --check,check --deploy, restart services. - Test the new server before DNS changes by adding
203.0.113.20 example.comto your local hosts file: log in, place a test order, upload a file.
5. Switch traffic
Update the A/AAAA records (or move a floating or reserved IP, which is instant). With a 300-second TTL, most clients follow within minutes. Issue certificates on the new server: if DNS has not switched yet, use the DNS-01 challenge, or copy /etc/letsencrypt from the old server and run certbot renew --dry-run after the switch. Then disable maintenance mode.
6. After the move
Watch logs on both servers: requests still arriving at the old one show stale DNS caches. Keep the old server in maintenance mode, read-only, for a few days as a fallback, then take a final backup, decommission it, and wipe its disks. Restore the DNS TTL to a normal value.
Verification check: dig +short example.com returns the new IP from several resolvers, the new server's access log shows production traffic, row counts for key tables match the final dump, a newly uploaded file appears on the new server, and scheduled jobs run there.
Explanation
The data that changes during a move (database rows and uploaded files) determines the downtime; everything else can be prepared in advance. Pre-syncing media with rsync and rehearsing the database copy shrinks the final step to a short, measured window under a write freeze, which guarantees no writes are lost. Lowering the DNS TTL beforehand shortens how long clients keep the old address, and keeping the old server read-only gives a safe fallback while caches expire.
Edge cases / notes
- Floating IPs within one provider make the switch instant and avoid DNS propagation entirely.
- Email. If the server sends mail directly, update SPF records with the new IP before the move.
- Webhooks and allow-lists. Third parties that allow-list your server IP (payment providers, partner APIs) need the new address in advance.
- Upgrading at the same time. Moving and upgrading PostgreSQL or Python together is fine if rehearsed; change as little else as possible.
Next steps
- Practise restores with Backup and Restore PostgreSQL for Django.
- Show a friendly page during the freeze with Django Maintenance Mode During Deploys.
FAQ
How do I move Django to a new server without downtime?
Truly zero downtime needs database replication; otherwise a short write freeze while you do a final sync keeps data consistent.
Should I copy the old server's disk image?
Prefer rebuilding from provisioning scripts. It catches undocumented configuration and gives a clean OS.
How long does DNS take to switch?
About the TTL. Lower it to 300 seconds a day or two before the move.
What should I do with the old server?
Keep it read-only for a few days as a fallback, then back it up, decommission it, and wipe the disks.