Deploy Django with Dokku on Your Own Server

Run a Heroku-style git push deploy for Django on your own server with Dokku, the Postgres plugin, predeploy migrations, persistent media storage, and Let's Encrypt.

Problem statement

Dokku is an open-source, single-server platform that gives you a Heroku-like workflow on a VM you control: git push dokku main builds your app into a container, runs it, and routes traffic through Nginx. It is a good fit when a managed platform has become too expensive but you still want push-to-deploy instead of maintaining systemd units and release scripts by hand.

Django needs a few things Dokku does not do automatically:

  • run migrations once per deploy, before the new containers take traffic
  • persist uploaded media outside the disposable app container
  • trust Dokku's Nginx proxy for HTTPS detection
  • back up the Postgres service container

This guide sets up a Django app on a fresh Dokku server with PostgreSQL, zero-downtime checks, persistent storage, and TLS.

Dokku deploy flowgit push dokkubuilds an imagepredeploy taskmanage.py migrateHealthchecknew containerNginxswitches trafficA failed predeploy or healthcheck leaves the previous container serving.

Quick answer

  1. Install Dokku on a fresh Ubuntu server and add your SSH key.
  2. dokku apps:create myapp, install the Postgres plugin, postgres:create and postgres:link (this sets DATABASE_URL).
  3. Set config with dokku config:set: secret key, allowed hosts, DEBUG=False.
  4. Add a Procfile with a Gunicorn web process and an app.json with a predeploy migration.
  5. Mount persistent storage for media with dokku storage:mount.
  6. git push dokku main.
  7. Set the domain and enable Let's Encrypt; verify and schedule database exports.

Step-by-step solution

1. Install Dokku

Follow the install instructions for the current release from the Dokku documentation on a fresh Ubuntu LTS server (Dokku expects to own Docker and Nginx on the host). After installation, add your deploy key:

cat ~/.ssh/id_ed25519.pub | ssh root@SERVER_IP dokku ssh-keys:add admin

Point a DNS record such as www.example.com at the server before enabling TLS later.

2. Create the app and database

On the server:

dokku apps:create myapp

sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git
dokku postgres:create myapp-db
dokku postgres:link myapp-db myapp

Linking injects a DATABASE_URL environment variable into the app and restarts it. The database runs in its own container and is reachable from the app over Docker's network, not from the internet.

3. Configure environment variables

dokku config:set myapp \
  DJANGO_SECRET_KEY="$(openssl rand -base64 48)" \
  DJANGO_DEBUG=False \
  DJANGO_ALLOWED_HOSTS=www.example.com \
  DJANGO_CSRF_TRUSTED_ORIGINS=https://www.example.com

config:set restarts the app by default. Pass --no-restart when you set several values ahead of a deploy.

4. Prepare the Django project

Dependencies (requirements.txt):

Django==5.2.6
gunicorn==23.0.0
dj-database-url==3.0.1
psycopg[binary]==3.2.10
whitenoise==6.9.0

Settings:

import os
from pathlib import Path

import dj_database_url

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")

DATABASES = {"default": dj_database_url.config(conn_max_age=60)}

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/app/media"
app.json controls the deploy"scripts": {"dokku": {"predeploy": "python manage.py migrate --noinput"}},"healthchecks": {"web": [{"type": "startup", "path": "/healthz/", "attempts": 5}]}1231Runs once in the new image; failure keeps the old containers.2Traffic switches only after the new container answers.3Checked by container IP: pass it before host validation.Together they give Heroku-style release ordering on one serverdjango-deployment.com
Without the startup check, Dokku only waits a fixed number of seconds.

Add whitenoise.middleware.WhiteNoiseMiddleware directly after SecurityMiddleware. Dokku's Nginx proxy sets X-Forwarded-Proto, so trusting it is correct here.

Procfile:

web: gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 3 --access-logfile -

app.json in the repository root:

{
  "scripts": {
    "dokku": {
      "predeploy": "python manage.py migrate --noinput"
    }
  },
  "healthchecks": {
    "web": [
      {
        "type": "startup",
        "name": "web check",
        "path": "/healthz/",
        "attempts": 5
      }
    ]
  }
}
  • predeploy runs in a container built from the new image, after the build and before the new web containers start. A non-zero exit aborts the deploy and leaves the old containers running.
  • The startup healthcheck makes Dokku wait for the new container to answer on /healthz/ before switching traffic, and keep the old container if it never does.

Implement /healthz/ as a view that returns a static 200, and make sure the healthcheck request passes host validation. Dokku sends the check to the container directly, so handle that path in a middleware placed before anything that calls request.get_host() if you see 400 responses.

5. Static files

The default Python buildpack runs collectstatic during the build when it detects Django. Build logs show it; if it fails, the deploy stops. With WhiteNoise in place, Gunicorn serves hashed static files efficiently.

If you deploy with a Dockerfile instead of buildpacks, run collectstatic in the Dockerfile.

6. Persistent media storage

App containers are replaced on every deploy, so files written inside them are lost. Mount a host directory:

dokku storage:ensure-directory myapp
dokku storage:mount myapp /var/lib/dokku/data/storage/myapp:/app/media
dokku ps:restart myapp
host directory to container pathMedia uploadsHost/var/lib/dokku/data/storage/myappContainer/app/media (MEDIA_ROOT)Survives✓deploys, rebuilds, restartsThe container user must be able to write to the host directorydjango-deployment.com
Run dokku storage:ensure-directory to create the host path with the right owner.

storage:ensure-directory creates the host directory with ownership suitable for the default buildpack user. Serve /media/ either through a custom Nginx include or, better for growth, move uploads to S3-compatible object storage.

7. Deploy

From your laptop:

git remote add dokku dokku@SERVER_IP:myapp
git push dokku main

Watch for the build, collectstatic, the predeploy migration output, and the healthcheck result.

8. Domain and HTTPS

dokku domains:set myapp www.example.com

sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku letsencrypt:set --global email ops@example.com
dokku letsencrypt:enable myapp
dokku letsencrypt:cron-job --add

The cron job renews certificates before they expire. Remove the default myapp.SERVER vhost domain if you do not want the app reachable there.

9. Create an admin user and verify

dokku run myapp python manage.py createsuperuser
dokku run myapp python manage.py check --deploy
dokku ps:report myapp
dokku logs myapp -t
curl -I https://www.example.com/

Verification check:

  • ps:report shows the web process running
  • curl returns 200 over HTTPS and HTTP redirects to HTTPS
  • check --deploy has no unexpected warnings
  • an uploaded file survives dokku ps:rebuild myapp

10. Back up the database

dokku postgres:export myapp-db > /var/backups/myapp-$(date +%F).dump

The export is a PostgreSQL custom-format dump. Schedule it with cron or a systemd timer and copy the file off the server. Test the restore path into a new service:

dokku postgres:create restore-test
dokku postgres:import restore-test < /var/backups/myapp-2026-09-01.dump

Rollback

Dokku keeps the previous image. Roll back by redeploying an earlier commit:

git push dokku <previous-good-sha>:refs/heads/main --force

That rebuilds the old code. Migrations already applied by the bad release stay applied, so keep schema changes backward compatible or take an export before deploying risky migrations.

Explanation

Dokku maps the twelve-factor workflow onto one host: buildpacks or a Dockerfile create an image, config:set provides configuration, the Procfile declares processes, and Nginx routes the domain to whichever containers are current. Django-specific release work belongs in app.json deployment tasks, which gives you the same "migrate once, then start new code" ordering Heroku's release phase provides.

Edge cases / notes

Deploy fails with a healthcheck timeout

Check dokku logs myapp for tracebacks. Common causes are a missing config variable, a DisallowedHost response to the check, or Gunicorn not binding to $PORT.

Celery workers

Add worker: celery -A config worker --loglevel=info to the Procfile, then dokku ps:scale myapp worker=1. Link a Redis service with the Redis plugin.

Resource limits

All apps share one host. Use dokku resource:limit to cap memory per process type so one app cannot starve the others.

Troubleshooting Dokku deploys

Dokku prints the whole pipeline during git push, so the push output is the first log to read. Match the failing phase:

Phase in push outputFailureWhere to dig
-----> Building myapp from herokuishdependency or collectstatic errorbuild output; reproduce with DJANGO_DEBUG=False python manage.py collectstatic locally
-----> Executing predeploy taskmigration errortask output in the push log
-----> Running healthchecks / Waiting forcontainer never answered the checkdokku logs myapp, then check /healthz/ handling and $PORT binding
After deploy, 502 Bad Gatewayapp crashed after switchingdokku logs myapp -t, dokku ps:report myapp
why a Dokku deploy stoppedSymptomUsual causeHealthcheck timeout400 from ALLOWED_HOSTSon the check pathpredeploy failedmigration error, orDATABASE_URL not linkedBuild killedserver out of memoryduring pip install413 on uploadsnginx client-max-body-size too smalldokku logs myapp -t and the deploy output name the failing stepdjango-deployment.com
A failed deploy leaves the previous containers serving, so there is time to fix it.

Useful inspection commands on the server:

dokku config:show myapp          # prints config values, including secrets
dokku ps:report myapp            # process status and restart policy
dokku nginx:show-config myapp    # generated Nginx config for the app
dokku enter myapp web            # shell inside the running web container
dokku postgres:info myapp-db     # service status and connection details

config:show prints secrets in plain text; avoid running it in shared terminal sessions or CI logs.

Resource and upgrade hygiene

  • Disk space: each deploy builds a new image. Old images and build cache accumulate. Check docker system df and prune with dokku cleanup and docker image prune periodically.
  • Dokku upgrades: read the release notes before upgrading Dokku or its plugins; upgrades are occasionally not backward compatible for plugin commands.
  • Postgres major versions: the Postgres plugin creates services with a specific image version. Upgrading to a new major version means exporting, creating a new service with the new image, and importing.

Uploads larger than the default limit

Dokku's Nginx configuration limits request bodies. If uploads fail with 413 Request Entity Too Large, raise the limit per app:

dokku nginx:set myapp client-max-body-size 20m
dokku proxy:build-config myapp

Keep Django's DATA_UPLOAD_MAX_MEMORY_SIZE and FILE_UPLOAD_MAX_MEMORY_SIZE in mind too; the proxy limit only lets the request reach Django.

Common mistakes

  • Setting DJANGO_DEBUG=True temporarily with config:set and forgetting to revert it.
  • Linking the database after the first deploy without redeploying or restarting; DATABASE_URL is only read at process start.
  • Pushing a branch other than the one Dokku deploys (by default main or master, depending on configuration); check with dokku git:report myapp.

Next steps

FAQ

How do I run Django migrations automatically with Dokku?

Add a predeploy script to app.json under scripts.dokku, such as python manage.py migrate --noinput. It runs once per deploy in a container from the new image, before new web containers start.

Where does Dokku store the PostgreSQL data?

The Postgres plugin stores data in a host directory mounted into the service container, so it survives container restarts. It is still on the same server, so export backups off the host.

Why are uploaded files lost after a Dokku deploy?

Each deploy starts fresh containers from the new image. Mount persistent storage for MEDIA_ROOT with dokku storage:mount, or store uploads in object storage.

Does Dokku support zero-downtime Django deploys?

Yes, for the web process. Dokku starts new containers, waits for healthchecks to pass, and then switches Nginx to them. Your migrations must stay compatible with the old code for that brief overlap.

Keep going