Skip to content

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 as a one-off job.

Abhishek Jaiswal

6 min

On this page 10 sections

A Django 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 a way to run migrations before code that needs them takes traffic. Ownkube covers all four without a cloud account or hand-written YAML.

This walks through deploying a standard Django app on Ownkube Compute: swapping the dev server for Gunicorn, wiring up a managed Postgres, handling static files with WhiteNoise, and running migrate as a job.

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 one-off job, so it runs once per release instead of on every container start.

1. Prepare settings for production

Most of this is standard Django. Point the database and static file settings at environment variables instead of hardcoded values:

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 lets a small app serve its own CSS and JS without a separate static file host or CDN.

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:

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’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) to deploy on every push, or deploy the current directory with the okctl CLI:

Terminal window
okctl login
okctl up --name django-app --port 8000

Ownkube builds the image from your Dockerfile, allocates a hostname on *.ownkube.app, and issues TLS automatically, so there is no load balancer or certificate to configure by hand. Running okctl up again ships the next version.

4. Add a managed Postgres database

Create a database box from the dashboard: New > Database > Postgres. It provisions a private (in-region) Postgres box starting at $4 a month plus storage at $0.25 per GB, with backups and point-in-time recovery available on Personal and Team.

The database runs as a single private instance by default, which is the normal shape for a small to mid-size Django app; opt-in public access and, on Team, high availability are per-database options when you need them.

Link the database to the app. Ownkube writes its connection string into the app as a secret DATABASE_URL variable and redeploys it:

Terminal window
okctl deploy link django-app <database-id>

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 in the dashboard and keep them out of the repository:

Terminal window
okctl vars set django-app SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(50))')" --secret
okctl vars set django-app DJANGO_SETTINGS_MODULE=myproject.settings ALLOWED_HOSTS="django-app.ownkube.app"

Environment variables set through Ownkube are encrypted at rest and only injected into the running container.

6. Run migrations as a job

Run migrate as a one-off job instead of on every container start. Create a Job deployment from the same repository, set its command to python manage.py migrate --noinput, and leave the schedule empty. Variables are set per deployment, so give the job the same SECRET_KEY and DJANGO_SETTINGS_MODULE and link the same database to it.

Then trigger a run whenever a release includes new migrations:

Terminal window
okctl deploy link django-migrate <database-id>
okctl deploy job-runs trigger django-migrate
okctl deploy job-runs list django-migrate

The run’s output is on the job’s Logs tab. The web app and the job deploy independently, so keep each migration safe for the version already running: add columns and tables in one release, and drop old ones in a later one.

7. Scale and watch it run

A fresh Django app usually starts fine on a small metered ceiling, for example a 500m CPU limit and 1 GiB of memory, set on the sliders under Settings → Resources.

Turn on horizontal autoscaling if traffic is uneven, so Ownkube adds Gunicorn instances under load and scales back down when it is quiet. On Ownkube Compute you turn it on through your coding agent (the update_deployment MCP tool) or the API, with a minimum and maximum replica count and a CPU target such as 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 a client’s security review asks for the app to run in a named AWS account, your own or theirs, the same Dockerfile and Django app move there with no rewrite. Ownkube provisions the instance, wires up the same deploy flow, and brings the logs, metrics, and health checks with it.

Railway, by contrast, only runs your workload on its own infrastructure. Our Railway alternatives guide compares Railway with the other options.

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 run as a one-off job, started from the job’s Runs tab or with okctl deploy job-runs trigger, instead of inside 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 credit from a quiet month carries into the next.

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 broken release or settings change does not require a manual recovery. A rollback restores the previous image, and a migration that already ran stays applied, which is one more reason to keep migrations additive.

For the always-current version of this walkthrough, see the Deploy Django guide in the docs.

Where Ownkube fits

Django wants a real database, a way to run 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 run migrations as a job. See Ownkube pricing for the per-minute wallet model, from $5 a month. Deploy my first app.

Abhishek Jaiswal · Founder and Engineer

Abhishek Jaiswal is the founder and engineer behind Ownkube, a cloud for personal software. He builds the platform and writes here about cloud cost, migrations, and running production infrastructure in your own AWS account without a DevOps team.

Share

More posts

All posts

Ship my first app in the next five minutes.

Push a repo and get a live URL, with no cloud account or config.

Top up from $5, billed per minute with no subscription.