EthioDeploy/Docs
HomePricing
Overview

Overview

  • Getting started
  • How deploys work

Frameworks

  • Framework guides

Configuration

  • Web services & background workers
  • Environment variables
  • Addons
  • Database & Redis tools
  • Branch deployments
  • Custom domains
  • Analytics

Domains

  • Buying a domain

Agent mode

  • Agent mode
  • Agent plans & credit

CLI

  • Command-line tool

Account

  • Team access
  • Billing

Help

  • Troubleshooting
  • Still stuck?
Deploy guides
  • Next.js
  • Node.js
  • Python
  • SvelteKit
  • Django
  • FastAPI
  • Flask
  • Express
  • Laravel
  • Go
  • Lovable
  • Bolt
  • Firebase Studio
  • Claude Code & Cursor
Compare
  • AI coding agent
  • Hosting in Ethiopia
  • vs Heroku
  • vs Vercel
  • vs Render
  • vs Railway
  • vs Fly.io
  • vs Netlify
  • Heroku alternatives
Deploy guidesDjango

Deploy a Django app

Django is detected from manage.py and started with migrate plus gunicorn, no Procfile needed. Update settings.py for production, add Postgres, and push.

Last updated October 23, 2026

Detected by
manage.py with a WSGI_APPLICATION setting
Install
pip install -r requirements.txt (or poetry / uv / pdm)
Build
none by default (add collectstatic)
Start
python manage.py migrate && gunicorn <project>.wsgi

These are the defaults. Override any of them in Settings → Build.

Set ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS before the first deploy

With DEBUG = False, Django answers every request for a host that isn't in ALLOWED_HOSTS with a 400, and rejects form posts from origins missing from CSRF_TRUSTED_ORIGINS with a 403. Add your <project-slug>.ethiodeploy.com address (and any custom domain) to both. CSRF entries need the https:// scheme.

Prepare your app

Add the production dependencies to requirements.txt:

django
gunicorn
whitenoise
dj-database-url
psycopg[binary]

We detect Django from manage.py and read WSGI_APPLICATION from your settings to build the start command, so keep that setting (every startproject has it). Each deploy runs python manage.py migrate and then gunicorn <project>.wsgi. Gunicorn reads the PORT variable we set and binds 0.0.0.0, so there is nothing to configure for the port.

Pin the Python version with a .python-version file (for example 3.13). A Heroku-style runtime.txt is not read.

Production settings

Read everything secret or host-specific from the environment:

import os
import dj_database_url

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG") == "1"

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS if h]

# The platform terminates HTTPS and forwards the original scheme
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

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

Then set ALLOWED_HOSTSin the project's env vars to a comma-separated list, for example myapp.ethiodeploy.com,myapp.com,www.myapp.com.

Static files with WhiteNoise

Nothing runs collectstatic by default. Serve static files from the app itself with WhiteNoise:

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

STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Set the Build command in Settings → Build to python manage.py collectstatic --noinput. Env vars are available during the build, so settings that read SECRET_KEY still load.

User uploads (MEDIA_ROOT) are different: each deploy starts a fresh container, so files written to disk are lost on the next push. Store uploads in S3-compatible object storage with django-storages.

Health check

Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving.

The check is sent with Host: <project-slug>.ethiodeploy.com, so keep that address in ALLOWED_HOSTS even after you add a custom domain. Without it Django answers the check with a 400 and the deploy fails.

Add a database

Add a Postgres (or MySQL) addon and DATABASE_URL is injected automatically. dj_database_url.config() in the settings above reads it. Migrations run on every start, so a new model goes live with the push that adds it. The database is private to the project: browse it with the built-in schema browser and SQL editor.

If you replace the start command (for example to run ASGI), keep the migration step in it:

python manage.py migrate && gunicorn myproject.asgi:application -k uvicorn_worker.UvicornWorker

Celery and background jobs

Addons are private to the project that owns them, so a separate worker project can't reach the same database or Redis. For a small app, run the worker in the same container by setting the Start command to:

celery -A myproject worker --concurrency 2 & python manage.py migrate && gunicorn myproject.wsgi

Add a Redis addon and point Celery at REDIS_URL. The worker isn't restarted on its own if it crashes, so watch the Logs tab. On the Free plan the container has 256 MB of memory, which fits a small app with one or two gunicorn workers but not much else.

Deploy

  1. Sign in with GitHub and install the EthioDeploy app on the repo.
  2. Click “New Project” and pick the repo.
  3. Watch the build in the dashboard. When the health check passes, your app is live at <project-slug>.ethiodeploy.com. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.

Troubleshooting

Build fails: Failed to find your WSGI_APPLICATION django setting

Cause: The settings module has no WSGI_APPLICATION, so we can't build the start command.

Fix: Add WSGI_APPLICATION = "myproject.wsgi.application", or set your own Start command.

Bad Request (400) on every page

Cause: The domain isn't in ALLOWED_HOSTS.

Fix: Add the ethiodeploy.com subdomain and every custom domain.

Deploy fails: GET / returned 400

Cause: The <project-slug>.ethiodeploy.com address isn't in ALLOWED_HOSTS, so Django rejects the health check.

Fix: Add it to ALLOWED_HOSTS, alongside any custom domain.

CSRF verification failed (403) on login or forms

Cause: CSRF_TRUSTED_ORIGINS is missing the https:// origin, or the proxy header isn't trusted.

Fix: Add the full https://… origin and set SECURE_PROXY_SSL_HEADER.

Admin has no CSS, or a ValueError about a missing manifest entry

Cause: collectstatic never ran.

Fix: Set the Build command to python manage.py collectstatic --noinput.

Uploaded files disappear after a push

Cause: Each deploy starts a new container with a fresh filesystem.

Fix: Store media in object storage with django-storages.

Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.

Questions

Can I host a Django app for free?

Yes. The Free plan runs one project with a 512 MB Postgres database at no cost. The app sleeps after 30 minutes without traffic and wakes on the next request, and the container has 256 MB of memory. Pro ($10/month) keeps it always on with 1 GB.

Do I need a Procfile or Dockerfile for Django?

No. Django is detected from manage.py and started with migrate and gunicorn automatically. If you have a Procfile from Heroku, its web line is used as the start command instead.

How do I run migrations on deploy?

The default start command runs python manage.py migrate before gunicorn on every deploy. If you set your own start command, begin it with python manage.py migrate &&.

Which Python version is used?

Add a .python-version file with the version you want, such as 3.12 or 3.13. A runtime.txt file from Heroku is not read.

Ready to deploy? Sign in with GitHub and pick your repo. No Dockerfile, no YAML.

Other framework guides

Next.jsNode.jsPythonSvelteKitLovableBoltClaude Code & CursorFastAPIFlaskLaravelExpressGoFirebase Studio

See the full docs for custom domains, env vars, addons, and billing.

On this page

  • Prepare your app
  • Production settings
  • Static files with WhiteNoise
  • Health check
  • Add a database
  • Celery and background jobs
  • Deploy
  • Troubleshooting
  • Questions

Product

  • Pricing
  • Documentation
  • AI coding agent
  • Agent mode docs
  • Domains
  • Hosting in Ethiopia

Deploy guides

  • Next.js
  • Node.js
  • Python
  • SvelteKit
  • Django
  • FastAPI
  • Flask
  • Express
  • Laravel
  • Go
  • Lovable
  • Bolt
  • Firebase Studio
  • Claude Code & Cursor

Compare

  • EthioDeploy vs Heroku
  • EthioDeploy vs Vercel
  • EthioDeploy vs Render
  • EthioDeploy vs Railway
  • EthioDeploy vs Fly.io
  • EthioDeploy vs Netlify
  • Heroku alternatives

Company

  • About
  • Support
  • Contact
  • Terms
  • Privacy

EthioDeploy: agentic first development and deployment, started in Ethiopia, built for developers everywhere.