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.
Quick answer
- Install Dokku on a fresh Ubuntu server and add your SSH key.
dokku apps:create myapp, install the Postgres plugin,postgres:createandpostgres:link(this setsDATABASE_URL).- Set config with
dokku config:set: secret key, allowed hosts,DEBUG=False. - Add a
Procfilewith a Gunicornwebprocess and anapp.jsonwith apredeploymigration. - Mount persistent storage for media with
dokku storage:mount. git push dokku main.- 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"
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
}
]
}
}
predeployruns 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
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:reportshows the web process runningcurlreturns200over HTTPS and HTTP redirects to HTTPScheck --deployhas 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 output | Failure | Where to dig |
|---|---|---|
-----> Building myapp from herokuish | dependency or collectstatic error | build output; reproduce with DJANGO_DEBUG=False python manage.py collectstatic locally |
-----> Executing predeploy task | migration error | task output in the push log |
-----> Running healthchecks / Waiting for | container never answered the check | dokku logs myapp, then check /healthz/ handling and $PORT binding |
After deploy, 502 Bad Gateway | app crashed after switching | dokku logs myapp -t, dokku ps:report myapp |
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 dfand prune withdokku cleanupanddocker image pruneperiodically. - 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=Truetemporarily withconfig:setand forgetting to revert it. - Linking the database after the first deploy without redeploying or restarting;
DATABASE_URLis only read at process start. - Pushing a branch other than the one Dokku deploys (by default
mainormaster, depending on configuration); check withdokku git:report myapp.
Next steps
- Compare with the hosted equivalent in Deploy Django on Heroku with Postgres.
- Pick and secure a server with Deploy Django on a Hetzner Cloud Server.
- Make restores routine with Backup and Restore PostgreSQL for Django Apps.
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.