Deploy a Django app to a VPS with Pystrano

A practical guide for a Django app running behind Gunicorn and systemd on a VPS-style Linux server.

Pystrano helps automate deployment steps. It does not replace server hardening, backups, monitoring, TLS setup, database administration, or full infrastructure provisioning.

What this guide covers

This guide covers a Django deployment flow where Pystrano clones your Git repository into a timestamped release directory, installs Python dependencies, links shared files, optionally runs migrations and static collection, updates the current symlink, restarts a systemd service, and prunes old releases.

Server assumptions

  • You have a VPS-style Linux server where you can SSH as root for setup.
  • The server can reach your Git repository over SSH.
  • System packages can be installed with apt.
  • Gunicorn is managed by a systemd service file that you provide.
  • Database, TLS, firewall, backups, and monitoring are handled outside Pystrano.

Django project assumptions

  • Your repository contains a requirements.txt file.
  • Your Django project can run manage.py migrate and manage.py collectstatic --noinput.
  • Your production settings read environment variables from the environment Pystrano loads from your dotenv file.
  • Your Gunicorn systemd service points at the release exposed through the current symlink.

Example deployment config

common:
  source_code_url: "git@github.com:example/example-django-app.git"
  project_root: "apps/example-django-app"
  project_user: "deploy"
  venv_dir: ".venv"
  keep_releases: 5
  system_packages: |
    libpq-dev
    python3-dev
    build-essential
  env_file: "./deploy/api/production/.env"
  ssh_known_hosts: "github.com"
  service_file: "./deploy/api/production/gunicorn.service"
  secrets: "./deploy/api/production/django-secret-key.txt"
  branch: "main"
  clone_depth: 1

servers:
  - host: "app1.example.com"
    port: 22
    run_migrations: true
    collect_static_files: true

Gunicorn and systemd notes

When service_file is configured, setup copies it to /etc/systemd/system/<service_file_name>, reloads systemd, and enables the service. Deploy restarts that service after promoting the new release.

Your service should be written for your app. In many setups it will use the virtualenv under /home/<project_user>/<venv_dir> and the app code under /home/<project_user>/<project_root>/current.

Environment variables and secrets

Pystrano copies the configured env_file into the server's shared directory during deploy and links the active shared dotenv path. Optional files listed in secrets are copied during setup and linked into each release during deploy. Do not commit real secrets to Git.

Running migrations

Set run_migrations: true on the server that should run Django migrations. Pystrano runs:

python manage.py migrate

In multi-server deployments, usually only one app server should run migrations.

Collecting static files

Set collect_static_files: true on servers where static collection should run. Pystrano runs:

python manage.py collectstatic --noinput

Deploying

Preview first:

pystrano deploy production api --dry-run

Deploy when ready:

pystrano deploy production api

Rolling back

Pystrano keeps timestamped release directories and promotes a release by updating the current symlink. It does not currently include a rollback CLI command. Keep keep_releases high enough for your recovery window and document a tested manual rollback procedure for your servers.

Troubleshooting

  • Use --dry-run before live changes to inspect remote commands.
  • Use --verbose when Fabric, Invoke, or Paramiko output is needed.
  • Check SSH access for both root and the configured project_user.
  • Confirm your Git host is included in ssh_known_hosts and that the server can clone the repository.
  • Confirm local paths referenced by env_file, service_file, and secrets exist before running setup or deploy.