Deploy Django on Koyeb

Deploy a Django app on Koyeb from GitHub or a Docker image: configure Gunicorn and the PORT, add a PostgreSQL database, set secrets, health checks, and regions, run migrations safely, serve static files with WhiteNoise, and attach a custom domain.

Problem statement

You want a managed platform that builds Django from a Git repository or runs a Docker image, gives you HTTPS and global regions, and does not require managing servers. Koyeb is a serverless container platform in that space. Like other PaaS hosts, it expects your app to listen on a given port, read configuration from the environment, and keep no state on local disk, so a few Django settings and a start command decide whether the first deploy works.

Django on KoyebGit or imagebuildpack or DockerServiceGunicorn on PORTPostgresmanaged or externalDomain + TLSautomaticKoyeb builds and runs the container; Django must bind the port and keep state elsewhere.
  1. Add gunicorn, whitenoise, dj-database-url, and psycopg[binary] to requirements.
  2. Start command: gunicorn config.wsgi --bind 0.0.0.0:$PORT --workers 2, with the service port set to the same value (8000 by default).
  3. Create a PostgreSQL database (Koyeb's managed Postgres or an external provider) and set DATABASE_URL as a secret.
  4. Set SECRET_KEY, ALLOWED_HOSTS (including .koyeb.app), and CSRF_TRUSTED_ORIGINS.
  5. Run migrations in a separate one-off step or a release command, not in the web start command of every instance.

Step-by-step solution

1. Prepare settings for a container platform

import os
import dj_database_url

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", ".koyeb.app").split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h.lstrip('.')}" for h in ALLOWED_HOSTS if h and not h.startswith(".")]
DATABASES = {"default": dj_database_url.config(conn_max_age=60, conn_health_checks=True, ssl_require=True)}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Add whitenoise.middleware.WhiteNoiseMiddleware after SecurityMiddleware. Koyeb terminates TLS at its edge and forwards X-Forwarded-Proto.

2. Choose how Koyeb builds the app

  • Buildpack: connect the GitHub repository; Koyeb detects Python from requirements.txt or pyproject.toml. Set the run command explicitly and add a build step for collectstatic (for example, in a Procfile or build command).
  • Dockerfile: Koyeb builds it, or you push an image to a registry and deploy that. A Dockerfile makes the build reproducible and lets you run collectstatic at build time.
how the Django image is builtBuildpack from Git✓no Dockerfile needed✓auto-detects Python✕less control over buildDockerfile or prebuilt image✓same image everywhere✓collectstatic at build✕you maintain the fileUse a Dockerfile once you need system packages or exact buildsdjango-deployment.com
Either way, the run command and port must match.

3. Configure the service

In the service settings:

  • Run command: gunicorn config.wsgi --bind 0.0.0.0:$PORT --workers 2 --timeout 60
  • Port: the same value as PORT (8000 unless you change it), exposed on path /.
  • Health check: HTTP on /healthz/, a view that returns 200 without redirects or host problems.
  • Instance type and regions: start with one region close to your database.

Binding to 127.0.0.1 or a different port than the one configured makes health checks fail and the deploy never becomes healthy.

4. Add the database and secrets

Create a PostgreSQL database in the same region, or use Neon, Supabase, or another managed provider. Store DATABASE_URL, SECRET_KEY, and API keys as Koyeb secrets and reference them in the service's environment variables. Keep the database in the same region as the service; cross-region queries add latency to every request.

5. Run migrations

Running migrate in the start command makes every instance attempt it on every restart. Prefer a separate step: deploy a one-off job or run the command from CI against the database before switching the service to the new version. For small single-instance apps, a start command of python manage.py migrate --noinput && gunicorn ... works but delays readiness.

before you click deployGunicorn binds 0.0.0.0:$PORTport matches serviceALLOWED_HOSTS has .koyeb.appplus custom domainDATABASE_URL as a secretsame region, sslmodeWhiteNoise and collectstaticstatic files servedHealth check on /healthz/returns 200Port, hosts, and database settings cause most failed first deploysdjango-deployment.com
Media uploads need object storage; the container disk is temporary.

Explanation

Koyeb runs your app as containers behind its global edge network, which terminates TLS and routes requests to healthy instances. Each deploy builds an image, starts new instances, waits for health checks, and then shifts traffic. That model rewards the same practices as every container platform: listen on the provided port, read settings from the environment, collect static files at build time, and store uploads and data in external services.

Edge cases / notes

  • Scale to zero. If enabled for a service, the first request after idle starts an instance and is slower; keep at least one instance for production traffic.
  • Workers. Run Celery as a separate worker service with the same image and a different command, without a public port.
  • Media files. Use S3, R2, or another object store with django-storages.

Next steps

FAQ

Does Koyeb need a Dockerfile for Django?

No. It can build from Git with buildpacks. A Dockerfile gives more control.

Which port should Gunicorn use?

The port configured for the service, available as PORT. Bind to 0.0.0.0, not localhost.

Where should media uploads go?

To object storage such as S3 or Cloudflare R2. Instance disks are not persistent.

How do I run Celery on Koyeb?

As a separate worker service using the same image with a celery command and no exposed port.

Keep going