The Django Deployment Checklist I Got Tired of Running by Hand
How a simple Django VPS deployment checklist slowly turns into something that should be versioned, reviewed, and automated.
The Django Deployment Checklist I Got Tired of Running by Hand
How a simple Django VPS deployment checklist slowly turns into something that should be versioned, reviewed, and automated.
Photo by Bernd 📷 Dittrich on Unsplash
Every Django deployment starts with a checklist.
At first, it is simple.
SSH into the server. Pull the latest code. Install dependencies. Run migrations. Collect static files. Restart Gunicorn. Check the logs.
If you are deploying a single small app to a single server, this can work for a long time.
The problem starts when the checklist grows.
You add staging. You add another server. You change a systemd service. You move the environment file. One server runs migrations, but not the other. You set up the same project again and realize half the process lives only in your memory.
That is when deployment stops being a checklist and becomes a risk.
This is one reason I started building Pystrano: to automate and streamline deployments, reducing manual steps and minimizing human error. With Pystrano, deployment processes are defined in clear, version-controlled files, which makes every step transparent and consistent across all environments. This not only speeds up deployments but also makes them safer and more reliable, allowing teams to focus on development instead of troubleshooting manual errors.
The checklist usually starts fine
A basic Django deployment is not complicated. It often looks something like this:
ssh deploy@app.example.com
cd /home/deploy/apps/my-django-app/current
git pull origin main
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn.service
sudo systemctl status gunicorn.service
There is nothing wrong with this.
The commands are clear. The process is understandable. You know what each step does.
This is also why many people keep deploying this way for much longer than they expected.
The problem is not that the checklist is bad.
The checklist eventually becomes too important to trust to manual execution.
Small mistakes are enough
Manual deployments usually fail in predictable ways.
Not because someone did something dramatic, but because one small thing was missed.
You pull the wrong branch.
You forgot to activate the virtualenv.
You run migrations on the wrong server.
You forgot to collect static files.
You restarted the wrong systemd service.
You edited the wrong .env file.
You deploy staging one way and production another way.
Each mistake is small, but during deployment, mistakes matter.
This is especially true when you are tired, hurried, or under pressure to fix something.
A deployment process should not depend on how well you remember a checklist at that moment.
The checklist is documentation
I do not think deployment checklists are bad.
Actually, they are often the most honest documentation a project has.
They describe what really happens when the app is deployed.
The issue is where the checklist lives.
If it lives in a private note, it is difficult to review.
If it lives in shell history, it is easy to lose track.
If it lives only in your head, no one else can use it safely.
Deployment instructions should live close to the project.
They should be versioned, reviewable, and make environment-specific decisions visible.
That was the idea behind Pystrano.
What should not live in memory?
A Django deployment has many small decisions. For example:
- Which repository should be deployed?
- Which branch should production use?
- Which Unix user should run the app?
- Where should the project live on the server?
- Where should the virtualenv be created?
- Which environment file should be copied?
- Which systemd service should be installed?
- Which server should run migrations?
- Should static files be collected?
- How many old releases should be kept?
None of these is complicated on its own.
But together, they become the deployment process.
If those decisions matter, document them in a structured way.
Turning the checklist into YAML
Pystrano uses YAML to describe Django deployment workflows, providing a transparent, repeatable, and auditable deployment process. Each deployment step is tracked and reviewed, so your team always knows what will happen and why.
A simplified configuration might look like this:
common:
source_code_url: "git@github.com:example/my-django-app.git"
project_root: "apps/my-django-app"
project_user: "deploy"
venv_dir: ".venv"
branch: "main"
keep_releases: 5
env_file: "./deploy/production/.env"
service_file: "./deploy/production/gunicorn.service"
servers:
- host: "app1.example.com"
port: 22
run_migrations: true
collect_static_files: true
- host: "app2.example.com"
port: 22
run_migrations: false
collect_static_files: true
This does not hide what is happening.
It is meant to make the deployment process visible.
When the deployment workflow is in a file, you can review it, change it intentionally, see why production differs from staging, and understand what will happen before touching the server.
This is much better than hoping the right commands remain in your terminal history.
Setup is not deployment
One thing I wanted Pystrano to separate clearly is setup and deployment.
These are not the same thing.
Setup is preparing the server. It may include installing system packages, creating directories, configuring SSH known hosts, copying environment files, or installing service files.
Deployment is the process of shipping a new release of the app.
Pystrano keeps these flows separate:
pystrano setup production api
pystrano deploy production api
You can also preview what will run:
pystrano deploy production api --dry-run
I like this because deployment tools should never feel mysterious.
Before running commands on a server, you should be able to see what the tool plans to do.
Deploying in place is fragile
A common manual deployment pattern is to update the live application directory directly.
You can cd into the current project directory, pull the latest code, install dependencies, run commands, and restart the service.
This works until something fails midway.
The server may then be in a strange state. Some files are updated, others are not. Dependencies may have changed. The service may not have restarted correctly.
This is why release-oriented deployment is useful.
Instead of changing the current app directory directly, each deployment creates a new release directory. The active version is represented by a current symlink.
Conceptually, it looks like this:
apps/my-django-app/
releases/
20260519120000/
20260519123000/
shared/
current -> releases/20260519123000
This makes deployments easier to review.
It also means old releases can be kept around for a while instead of being overwritten immediately.
Rollback is not magic
Release directories make rollback easier to reason about.
But rollback is not magic.
If you only changed the application code, moving back to a previous release can be straightforward.
If you also ran database migrations, things are more complicated.
A migration may change the schema. It may modify data. It may not be safely reversible. The old code may not work with the new database state.
This is why I dislike pretending rollback is just a button.
A deployment tool can help structure releases. It can make the active version clearer. It can keep previous releases available.
But your migration strategy still matters.
Rolling back code is easy to describe, but rolling back a production system is harder.
What I wanted from a tool
I did not want a large platform.
I wanted something smaller.
A tool that could:
- Use SSH
- Read a simple YAML file and Django deployment steps.
- Separate setup from deploy
- Support dry runs
- Keep releases organized
- Make server-specific behavior explicit.
That became Pystrano: a tool designed to bring structure and reliability to Django deployments using SSH and YAML-based workflows. It offers clarity, consistency, and the peace of mind that comes from repeatable, auditable processes.
The goal is not to replace every deployment or infrastructure tool but to make a common Django deployment workflow more repeatable.
The goal is to make a common Django deployment workflow more repeatable.
What Pystrano does not solve
It is important to say what Pystrano does not do.
Pystrano does not replace server hardening.
It does not replace backups.
It does not replace monitoring.
It does not configure your database.
It does not manage TLS.
It does not provision cloud infrastructure.
These things still matter.
Pystrano focuses on deployment automation. It specifically targets Django deployment workflows over SSH, making them more reliable, auditable, and less dependent on memory or ad hoc scripts. By standardizing your process, Pystrano helps ensure every deployment is executed consistently, reducing surprises and saving time.
That focus is intentional.
I prefer making one workflow clear rather than claiming support for everything too early.
Why Django first
Django is the first supported workflow because it has a familiar deployment shape.
Most Django apps need some combination of:
- dependencies
- environment files
- migrations
- static files
- Gunicorn
- systemd
- release management
That gives Pystrano a concrete problem to solve.
FastAPI support is planned, but I do not want to call it supported until it is implemented, tested, and documented.
FastAPI deployments are different. They usually involve ASGI servers, optional Alembic migrations, and different service conventions.
That deserves its own workflow.
Final thoughts
The checklist was not the issue.
The issue was that the checklist became too important to run by hand.
When a deployment process matters, it should be visible, versioned, and repeatable. It should not depend on one person remembering every command in the right order.
That is the idea behind Pystrano.
A small, Django-first deployment tool inspired by Capistrano.
If you deploy Django apps to VPS-style servers and follow a checklist like this, try Pystrano.
- Website: https://pystrano.com
- GitHub: https://github.com/pystrano/pystrano
- PyPI: https://pypi.org/project/pystrano/
메타데이터
- post_id
- 7a29918fd3f7
- slug
- the-django-deployment-checklist-i-got-tired-of-running-by-hand-7a29918fd3f7
- url
- https://medium.com/@lexpank/the-django-deployment-checklist-i-got-tired-of-running-by-hand-7a29918fd3f7
- canonical_url
- https://medium.com/@lexpank/the-django-deployment-checklist-i-got-tired-of-running-by-hand-7a29918fd3f7
- author_url
- https://medium.com/@lexpank
- status
- ok
- fetched_at
- 2026-06-14 13:58:26