# How to deploy a Django app on Ownkube

> Deploy a Django app on Ownkube with Gunicorn, a managed Postgres database, WhiteNoise for static files, and migrations that run automatically on release.

- **Published:** 2026-08-24
- **Author:** Ownkube team
- **Category:** How-To
- **Tags:** django, python, deploy, tutorial, ownkube-compute
- **Canonical URL:** https://ownkube.io/blog/deploy-django-app
- **Cover:** https://ownkube.io/blog/deploy-django-app.png

---
A [Django](https://docs.djangoproject.com/en/stable/) app in production needs four things from a host: a real WSGI server in front of it, a Postgres database it can reach, static files served correctly, and migrations that run before the new code takes traffic. Ownkube covers all four from a git push, with no cloud account and no YAML to hand-write.

This walks through deploying a standard Django app on Ownkube Compute: swapping the dev server for Gunicorn, wiring up a [managed Postgres](/blog/add-managed-postgres-database), handling static files with WhiteNoise, and running `migrate` on release.

**TL;DR**

- Push a Django app with a `Dockerfile`, and Ownkube builds it, deploys it, and issues TLS automatically on your `*.ownkube.app` hostname.
- Add an Ownkube-managed Postgres database and point `DATABASE_URL` at it with `dj-database-url` or the equivalent.
- Serve static files with WhiteNoise instead of standing up a separate static host, and run `collectstatic` in the Docker build.
- Set `SECRET_KEY`, `DJANGO_SETTINGS_MODULE`, and `ALLOWED_HOSTS` as environment variables in the dashboard, not in the repo.
- Run `migrate` as a release step so it happens once, before Gunicorn starts serving the new version.

## 1. Prepare settings for production

Most of this is standard Django, not anything Ownkube-specific. Point the database and static file settings at environment variables instead of hardcoded values:

```python
# settings.py
import os
import dj_database_url

DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")

DATABASES = {
    "default": dj_database_url.config(env="DATABASE_URL", conn_max_age=600)
}

STATIC_URL = "/static/"
STATIC_ROOT = "/app/staticfiles"
STORAGES = {
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ...the rest of your middleware
]
```

Add `whitenoise`, `gunicorn`, `psycopg[binary]`, and `dj-database-url` to `requirements.txt`. [WhiteNoise](https://whitenoise.readthedocs.io/) means you do not need a separate static file host or CDN just to serve CSS and JS in front of a small app.

## 2. Write a Dockerfile

Ownkube deploys from a Dockerfile when the repo has one, so you get full control over the Python version and the build steps:

```dockerfile
FROM python:3.12-slim

RUN apt-get update -qq && apt-get install -y libpq-dev build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Collect static files at build time, before SECRET_KEY exists at runtime
RUN SECRET_KEY=build-only DATABASE_URL=sqlite:///build.db \
    python manage.py collectstatic --noinput

ENV PYTHONUNBUFFERED=1
EXPOSE 8000

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
```

[Gunicorn](https://docs.gunicorn.org/)'s worker count is a starting point. A CPU-bound app usually wants workers close to `(2 x vCPU) + 1`; an I/O-bound app that mostly waits on the database can run more workers per vCPU than that rule suggests.

## 3. Push the repo and create the app

Connect the repository from the Ownkube dashboard (`app.ownkube.io`) or push directly with the `okctl` CLI:

```bash
okctl login
okctl apps create django-app --dockerfile Dockerfile
okctl deploy
```

Ownkube builds the image, allocates a hostname on `*.ownkube.app`, and issues TLS automatically. No load balancer or certificate to configure by hand.

## 4. Add a managed Postgres database

Create a database box from the dashboard: **New > Database > Postgres**. It provisions a single-instance, private (in-region) Postgres box starting at $4 a month plus storage at $0.15 per GB, with backups included. Multi-instance HA, public access, and point-in-time recovery are on the roadmap and not shipped yet, so plan around a single private instance for now, which is the normal shape for a small to mid-size Django app anyway.

Attach the connection string as an environment variable:

```bash
okctl env set DATABASE_URL="postgres://user:password@db-host:5432/django_production" --app django-app
```

`dj_database_url.config()` in `settings.py` reads it directly, so there is no further Django-side configuration.

## 5. Set your secrets

Django needs `SECRET_KEY` to sign sessions and cookies, and it will refuse to run with `DEBUG=False` and an empty `ALLOWED_HOSTS`. Set these as [environment variables](/blog/manage-secrets-env-vars) in the dashboard rather than committing them:

```bash
okctl env set SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(50))')" --app django-app
okctl env set DJANGO_SETTINGS_MODULE=myproject.settings --app django-app
okctl env set ALLOWED_HOSTS="django-app.ownkube.app" --app django-app
```

Never commit `SECRET_KEY` to the repository. Environment variables set through Ownkube are encrypted at rest and only injected into the running container.

## 6. Run migrations on release

Migrations need to run before Gunicorn starts serving requests against the new code, not after. Configure `migrate` as a release command so Ownkube runs it once per deploy, ahead of the rollout:

```bash
okctl apps update django-app --release-command "python manage.py migrate --noinput"
```

With a release command set, every `okctl deploy` runs the migration against the connected Postgres box first, and only rolls Gunicorn workers over to the new image once it succeeds. If the migration fails, the previous version keeps serving traffic, which is the zero-downtime rollout behavior working in your favor rather than a race between old code and a half-migrated schema.

## 7. Scale and watch it run

A fresh Django app usually starts fine on a small metered ceiling:

```bash
okctl deploy --cpu-limit 500m --memory-limit 1Gi
```

Turn on [horizontal autoscaling](/blog/autoscale-cpu-memory) if traffic is uneven, so Ownkube adds Gunicorn instances under load and scales back down when it is quiet:

```bash
okctl apps update django-app --autoscale-min 1 --autoscale-max 4 --autoscale-cpu-target 70
```

Metered billing fits a typical Django app well: it charges CPU and memory on actual per-minute usage, and a Django app sitting between requests barely registers on the CPU line. Logs and metrics for the app and the database box both live in the dashboard, so a slow migration or a worker that keeps restarting shows up in one place.

## Moving to your own AWS later

If the app outgrows the shared Compute tier, or a client's security review asks where the app runs, the same Dockerfile and Django app move to your own AWS account with no rewrite. Ownkube provisions the instance, wires up the same deploy flow, and brings the logs, metrics, and health checks with it. That portability is one of the things that separates Ownkube from a platform like Railway, which only runs your workload on its own infrastructure. Our [Railway alternative comparison](/blog/railway-alternative-2026) covers the tradeoffs in more depth.

## FAQ

### Does Ownkube support Django's admin site and management commands?

Yes. The admin works like any other Django view once `ALLOWED_HOSTS` and static files are configured. One-off management commands can run as a scheduled job triggered manually from the dashboard's Runs tab, rather than exec'ing into a running web container.

### Do I need a Dockerfile, or can Ownkube detect Django automatically?

A Dockerfile gives you control over the Python version, system packages like `libpq-dev`, and the `collectstatic` build step, and is the most reliable path for Django. Simpler apps without native dependencies can also deploy straight from source using Ownkube's automatic build.

### Can I run Celery or another background worker alongside the web app?

Yes. Deploy the worker as a second Ownkube app pointed at the same Postgres connection string, and add a Valkey cache box from $2 a month if you need Redis as the broker, so the web app and worker scale independently.

### What happens if my wallet balance runs out mid-month?

The app pauses and its data, including the Postgres box, is kept. Adding credit or the next monthly plan load resumes it. Unused wallet credit rolls over and never expires, so a quiet month is not credit lost.

### Can I roll back a bad deploy?

Yes. Ownkube keeps prior image versions and supports zero-downtime rollback from the dashboard or `okctl`, so a bad migration or a broken settings change does not require a manual recovery.

## Where Ownkube fits

Django wants a real database, a release step for migrations, and a place to serve static files without standing up a separate CDN, and Ownkube gives you all three without provisioning a cloud account first. Push the Dockerfile, attach a managed Postgres, set your secrets, and the release command handles the rest. [Deploy your first app](https://app.ownkube.io/login).