DEV Community

Cover image for Automating Django `dumpdata` Backups to S3 (One Command, Zero SSH)
Vicente G. Reyes
Vicente G. Reyes

Posted on • Originally published at vicentereyes.org

Automating Django `dumpdata` Backups to S3 (One Command, Zero SSH)

If you're running a cookiecutter-django project on a droplet and still ssh-ing in every time you need a data snapshot, here's how I collapsed that into a single local command: dump → pull → upload to S3, no manual steps in between.

The Problem

My portfolio backend runs cookiecutter-django, containerized via Docker Compose, on a DigitalOcean droplet. Every time I wanted a fresh data snapshot — before a schema change, before a risky migration, or just as a periodic backup — the process looked like:

  1. SSH into the droplet
  2. cd into the project directory
  3. Run dumpdata inside the Django container
  4. Copy the resulting file out of the container
  5. scp it down to my local machine
  6. Manually upload it to S3 for offsite storage

Six steps, every time. That's the kind of manual toil that either doesn't happen often enough, or happens with copy-paste mistakes.

Step 1: Get dumpdata Right

The naive manage.py dumpdata > data.json works, but it dumps everything — including tables that don't need to be in a portable fixture and will actively cause problems if you ever loaddata this into a different environment:

python manage.py dumpdata \
  --natural-foreign --natural-primary \
  --exclude auth.permission \
  --exclude contenttypes \
  --exclude admin.logentry \
  --exclude sessions.session \
  --indent 2 \
  -o data.json
Enter fullscreen mode Exit fullscreen mode

Why these flags matter:

  • --natural-foreign --natural-primary — replaces hardcoded primary keys with natural lookups where possible. Without this, reloading the fixture into an environment with different PK sequences (a fresh DB, a staging environment) can silently corrupt relationships.
  • Excluding contenttypes, auth.permission, admin.logentry, sessions.session — these are either auto-regenerated by Django on any fresh install or pure operational noise. Including them bloats the dump and can cause loaddata conflicts if content types are ever re-registered in a different order.
  • --indent 2 — makes the output diffable in git if you're versioning snapshots, instead of one giant unreadable line.

Step 2: Run It Inside Docker Correctly

If your project is containerized, the command has to run inside the django service, not on the host:

docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata \
  --natural-foreign --natural-primary \
  --exclude auth.permission \
  --exclude contenttypes \
  --exclude admin.logentry \
  --exclude sessions.session \
  --indent 2 > data.json
Enter fullscreen mode Exit fullscreen mode

Two details that will bite you if skipped:

The -T flag disables TTY allocation. Without it, exec allocates a pseudo-terminal by default. Run this over a scripted or piped SSH session and you risk carriage returns (\r\n) getting injected into your output, silently corrupting the JSON. Always pass -T for non-interactive dumps.

Redirect happens on the right side of the pipe. Since docker compose exec streams its own stdout, running this directly on the host (or via ssh host "command") means the > redirect captures cleanly outsid# Automating Django dumpdata Backups to S3 (One Command, Zero SSH)

If you're running a cookiecutter-django project on a droplet and still ssh-ing in every time you need a data snapshot, here's how I collapsed that into a single local command: dump → pull → upload to S3, no manual steps in between.

The Problem

My portfolio backend runs cookiecutter-django, containerized via Docker Compose, on a DigitalOcean droplet. Every time I wanted a fresh data snapshot — before a schema change, before a risky migration, or just as a periodic backup — the process looked like:

  1. SSH into the droplet
  2. cd into the project directory
  3. Run dumpdata inside the Django container
  4. Copy the resulting file out of the container
  5. scp it down to my local machine
  6. Manually upload it to S3 for offsite storage

Six steps, every time. That's the kind of manual toil that either doesn't happen often enough, or happens with copy-paste mistakes.

Step 1: Get dumpdata Right

The naive manage.py dumpdata > data.json works, but it dumps everything — including tables that don't need to be in a portable fixture and will actively cause problems if you ever loaddata this into a different environment:

python manage.py dumpdata \
  --natural-foreign --natural-primary \
  --exclude auth.permission \
  --exclude contenttypes \
  --exclude admin.logentry \
  --exclude sessions.session \
  --indent 2 \
  -o data.json
Enter fullscreen mode Exit fullscreen mode

Why these flags matter:

  • --natural-foreign --natural-primary — replaces hardcoded primary keys with natural lookups where possible. Without this, reloading the fixture into an environment with different PK sequences (a fresh DB, a staging environment) can silently corrupt relationships.
  • Excluding contenttypes, auth.permission, admin.logentry, sessions.session — these are either auto-regenerated by Django on any fresh install or pure operational noise. Including them bloats the dump and can cause loaddata conflicts if content types are ever re-registered in a different order.
  • --indent 2 — makes the output diffable in git if you're versioning snapshots, instead of one giant unreadable line.

Step 2: Run It Inside Docker Correctly

If your project is containerized, the command has to run inside the django service, not on the host:

docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata \
  --natural-foreign --natural-primary \
  --exclude auth.permission \
  --exclude contenttypes \
  --exclude admin.logentry \
  --exclude sessions.session \
  --indent 2 > data.json
Enter fullscreen mode Exit fullscreen mode

Two details that will bite you if skipped:

The -T flag disables TTY allocation. Without it, exec allocates a pseudo-terminal by default. Run this over a scripted or piped SSH session and you risk carriage returns (\r\n) getting injected into your output, silently corrupting the JSON. Always pass -T for non-interactive dumps.

Redirect happens on the right side of the pipe. Since docker compose exec streams its own stdout, running this directly on the host (or via ssh host "command") means the > redirect captures cleanly outside the container — no separate docker compose cp step needed, unlike if you'd used -o data.json inside the container itself.

Step 3: Collapse the Whole Thing Into One Alias

Here's the full pipeline — SSH in, dump, and stream the result straight to a local file — as a single shell alias:

alias dumpdata='ssh username@your-droplet-ip "cd ~/root/project && docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata --natural-foreign --natural-primary --exclude auth.permission --exclude contenttypes --exclude admin.logentry --exclude sessions.session --indent 2" > ~/Downloads/data.json'
Enter fullscreen mode Exit fullscreen mode

Run dumpdata from your local machine, and the entire remote pipeline executes and lands a clean data.json in your Downloads folder. No manual SSH session, no intermediate files left behind on the server.

Step 4: Ship It to S3

With AWS CLI configured (aws configure, or a named profile if you juggle multiple client accounts):

aws s3 cp ~/Downloads/data.json s3://your-bucket/backups/data-$(date +%Y%m%d-%H%M%S).json
Enter fullscreen mode Exit fullscreen mode

The timestamped key matters — without it, every backup silently overwrites the last one, and you lose the ability to roll back to an earlier snapshot.

If credentials aren't configured yet, aws s3 cp fails with Unable to locate credentials. Fix it with:

aws configure --profile your-profile
Enter fullscreen mode Exit fullscreen mode

Using a named profile instead of the default is worth the extra keystroke if you handle backups for multiple clients or projects — it keeps credential scope explicit instead of relying on whatever happens to be the account's default.

The One-Liner, Fully Chained

alias dumpdata='ssh username@your-droplet-ip "cd ~/root/project && docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata --natural-foreign --natural-primary --exclude auth.permission --exclude contenttypes --exclude admin.logentry --exclude sessions.session --indent 2" > ~/Downloads/data.json && aws s3 cp ~/Downloads/data.json "s3://your-bucket/backups/data-$(date +%Y%m%d-%H%M%S).json" --profile your-profile'
Enter fullscreen mode Exit fullscreen mode

One command. Dumps remotely, pulls locally, uploads to S3 with a timestamped key.

Where I'd Take It Next

An alias is fine for on-demand backups, but it has no failure handling — if the SSH connection drops mid-stream, or the dump comes back empty, you'll happily upload a corrupted or zero-byte file to S3 without noticing. The next iteration I'm planning is a small bash script with set -euo pipefail, a file-size sanity check before the S3 upload, and eventually a cron job on the droplet itself so backups happen on a schedule without me remembering to run anything at all.


*Originally documented as part of ongoing DevOps work on the ICVN Tech Studio portfolio infrastructure (cookiecutter-django on DigitalOcean, Cloudflare-fronted).*e the container — no separate docker compose cp step needed, unlike if you'd used -o data.json inside the container itself.

Step 3: Collapse the Whole Thing Into One Alias

Here's the full pipeline — SSH in, dump, and stream the result straight to a local file — as a single shell alias:

alias dumpdata='ssh username@your-droplet-ip "cd ~/root/project && docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata --natural-foreign --natural-primary --exclude auth.permission --exclude contenttypes --exclude admin.logentry --exclude sessions.session --indent 2" > ~/Downloads/data.json'
Enter fullscreen mode Exit fullscreen mode

Run dumpdata from your local machine, and the entire remote pipeline executes and lands a clean data.json in your Downloads folder. No manual SSH session, no intermediate files left behind on the server.

Step 4: Ship It to S3

With AWS CLI configured (aws configure, or a named profile if you juggle multiple client accounts):

aws s3 cp ~/Downloads/data.json s3://your-bucket/backups/data-$(date +%Y%m%d-%H%M%S).json
Enter fullscreen mode Exit fullscreen mode

The timestamped key matters — without it, every backup silently overwrites the last one, and you lose the ability to roll back to an earlier snapshot.

If credentials aren't configured yet, aws s3 cp fails with Unable to locate credentials. Fix it with:

aws configure --profile your-profile
Enter fullscreen mode Exit fullscreen mode

Using a named profile instead of the default is worth the extra keystroke if you handle backups for multiple clients or projects — it keeps credential scope explicit instead of relying on whatever happens to be the account's default.

The One-Liner, Fully Chained

alias dumpdata='ssh username@your-droplet-ip "cd ~/root/project && docker compose -f docker-compose.production.yml exec -T django python manage.py dumpdata --natural-foreign --natural-primary --exclude auth.permission --exclude contenttypes --exclude admin.logentry --exclude sessions.session --indent 2" > ~/Downloads/data.json && aws s3 cp ~/Downloads/data.json "s3://your-bucket/backups/data-$(date +%Y%m%d-%H%M%S).json" --profile your-profile'
Enter fullscreen mode Exit fullscreen mode

One command. Dumps remotely, pulls locally, uploads to S3 with a timestamped key.

Where I'd Take It Next

An alias is fine for on-demand backups, but it has no failure handling — if the SSH connection drops mid-stream, or the dump comes back empty, you'll happily upload a corrupted or zero-byte file to S3 without noticing. The next iteration I'm planning is a small bash script with set -euo pipefail, a file-size sanity check before the S3 upload, and eventually a cron job on the droplet itself so backups happen on a schedule without me remembering to run anything at all.


Originally documented as part of ongoing DevOps work on the ICVN Tech Studio portfolio infrastructure (cookiecutter-django on DigitalOcean, Cloudflare-fronted).

Top comments (0)