{
  "manifest_version": "1",
  "slug": "django",
  "title": "Django",
  "headline": "Deploy a Django app",
  "description": "Deploy Django to a VM of its own: git push to deploy, gunicorn and nginx, SQLite and uploads on a persistent disk, and the admin behind exe.dev login.",
  "category": "frameworks",
  "tags": [
    "django",
    "python",
    "gunicorn",
    "nginx",
    "sqlite",
    "uv",
    "git push deploy",
    "django admin",
    "heroku alternative"
  ],
  "url": "https://exe.dev/deploy/django",
  "markdown_url": "https://exe-staging.dev/deploy/django.md",
  "upstream": {
    "website": "https://www.djangoproject.com",
    "source": "https://github.com/django/django",
    "docs": "https://docs.djangoproject.com/en/6.1/howto/deployment/",
    "license": "BSD-3-Clause"
  },
  "example_vm_name": "my-django",
  "time": "10 minutes",
  "stack": [
    {
      "name": "Your Django app",
      "detail": "Django 6.1 on Python 3.14, installed by uv; one release per deploy",
      "data": "~/app"
    },
    {
      "name": "gunicorn",
      "detail": "app.service, the WSGI server",
      "port": 8001
    },
    {
      "name": "nginx",
      "detail": "static files, uploads, and a buffer in front of gunicorn",
      "port": 8000
    },
    {
      "name": "SQLite",
      "detail": "WAL mode, tuned for a web server",
      "data": "~/data/db.sqlite3"
    },
    {
      "name": "Uploads",
      "detail": "MEDIA_ROOT, served at /media/",
      "data": "~/data/media"
    },
    {
      "name": "git push deploys",
      "detail": "a bare repository with a deploy hook",
      "data": "~/app.git"
    },
    {
      "name": "Nightly backups",
      "detail": "database, uploads, settings, and code; keeps a week",
      "data": "~/backups"
    }
  ],
  "access": [
    {
      "url": "https://my-django.exe.xyz/",
      "who": "private",
      "what": "your app, until you make it public"
    },
    {
      "url": "https://my-django.exe.xyz/admin/",
      "who": "private",
      "what": "the Django admin, signed in with your exe.dev account"
    },
    {
      "url": "my-django.exe.xyz:app.git",
      "who": "private",
      "what": "where you `git push` to deploy"
    }
  ],
  "inputs": [
    {
      "name": "REPO",
      "description": "the GitHub repository to deploy, as owner/name or a URL, or `new` to start a fresh Django app"
    }
  ],
  "delivers": [
    "Your Django app, or a new Django 6.1 project, served by gunicorn behind nginx on port 8000, under systemd and enabled to start after a reboot.",
    "`git push exe main` deploys: the pre-receive hook in `~/app.git` builds and migrates each push in a new `~/releases` directory and switches `~/app` to it only if every step passes, so a failing build rejects the push and the running release stays.",
    "Production settings for exe.dev's proxy in `~/app-settings` and `~/app.env`, with DEBUG off, a generated secret key, HTTPS detected, and allowed hosts and CSRF origins set; `manage check --deploy` reports no issues.",
    "SQLite in WAL mode at `~/data/db.sqlite3`, and uploads in `~/data/media` served at `/media/`.",
    "The owner's exe.dev account is a Django superuser and opens `/admin/` without a password.",
    "Nightly backups in `~/backups`, with a first one taken and checked."
  ],
  "owner_runs_after": [
    "git clone -o exe my-django.exe.xyz:app.git my-django  # if you started a new project",
    "git remote add exe my-django.exe.xyz:app.git          # or, in your own repository's checkout"
  ],
  "deployable": true,
  "deploy": {
    "new_vm_url": "https://exe-staging.dev/new?prompt=REPO%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Django%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fdjango.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20REPO%3A%20the%20GitHub%20repository%20to%20deploy%2C%20as%20owner%2Fname%20or%20a%20URL%2C%20or%20%60new%60%20to%20start%20a%20fresh%20Django%20app%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-django.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-django.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20Your%20Django%20app%2C%20or%20a%20new%20Django%206.1%20project%2C%20served%20by%20gunicorn%20behind%20nginx%20on%20port%208000%2C%20under%20systemd%20and%20enabled%20to%20start%20after%20a%20reboot.%0A-%20%60git%20push%20exe%20main%60%20deploys%3A%20the%20pre-receive%20hook%20in%20%60~%2Fapp.git%60%20builds%20and%20migrates%20each%20push%20in%20a%20new%20%60~%2Freleases%60%20directory%20and%20switches%20%60~%2Fapp%60%20to%20it%20only%20if%20every%20step%20passes%2C%20so%20a%20failing%20build%20rejects%20the%20push%20and%20the%20running%20release%20stays.%0A-%20Production%20settings%20for%20exe.dev%27s%20proxy%20in%20%60~%2Fapp-settings%60%20and%20%60~%2Fapp.env%60%2C%20with%20DEBUG%20off%2C%20a%20generated%20secret%20key%2C%20HTTPS%20detected%2C%20and%20allowed%20hosts%20and%20CSRF%20origins%20set%3B%20%60manage%20check%20--deploy%60%20reports%20no%20issues.%0A-%20SQLite%20in%20WAL%20mode%20at%20%60~%2Fdata%2Fdb.sqlite3%60%2C%20and%20uploads%20in%20%60~%2Fdata%2Fmedia%60%20served%20at%20%60%2Fmedia%2F%60.%0A-%20The%20owner%27s%20exe.dev%20account%20is%20a%20Django%20superuser%20and%20opens%20%60%2Fadmin%2F%60%20without%20a%20password.%0A-%20Nightly%20backups%20in%20%60~%2Fbackups%60%2C%20with%20a%20first%20one%20taken%20and%20checked.%0A%0ARead%20the%20whole%20recipe%2C%20including%20the%20closed%20details%2C%20and%20follow%20%22Set%20it%0Aup%20by%20hand%22%20in%20order%2C%20skipping%20%22Create%20the%20VM%22%3B%20the%20walkthrough%20above%20it%0Ais%20for%20the%20owner.%20REPO%3Dnew%20takes%20%22Start%20a%20new%20project%22%3B%20anything%20else%0Atakes%20%22Bring%20your%20repository%22.%20If%20cloning%20from%20GitHub%20fails%2C%20the%0Arepository%20is%20private%3A%20give%20the%20owner%20the%20%60ssh%20exe.dev%20integrations%20add%0Agithub%60%20command%20from%20that%20step%2C%20with%20this%20VM%27s%20name%20and%20the%20repository%2C%0Await%20for%20them%2C%20and%20clone%20from%20github.int.exe.xyz.%0ANever%20overwrite%20an%20existing%20~%2Fapp.env.%20Don%27t%20commit%20to%20the%20owner%27s%20code%2C%0Aand%20don%27t%20push%20test%20commits%20to%20~%2Fapp.git%3A%20read%20the%20hook%20to%20check%20it.%0AIf%20their%20repository%20doesn%27t%20fit%20the%20deploy%20hook%20%28manage.py%20not%20at%20the%0Atop%2C%20Pipenv%2C%20a%20custom%20user%20model%20without%20a%20username%20field%29%2C%20adapt%20the%0Ahook%20or%20the%20command%20and%20tell%20the%20owner%20what%20you%20changed.%0ALeave%20the%20VM%20private%20and%20don%27t%20reboot%3A%20check%20that%20app.service%2C%20nginx%20and%0Aapp-backup.timer%20are%20enabled%20instead.%20Skip%20the%20later%20Restore%20and%20Update%0Ainstructions%3B%20the%20restore%20check%20in%20setup%20uses%20a%20scratch%20copy%20only.%0AThe%20URLs%20to%20give%20are%20https%3A%2F%2F%3Cvm%3E.exe.xyz%2F%20and%20%2Fadmin%2F.%20Of%20the%20two%0Acommands%20at%20the%20end%2C%20give%20the%20owner%20only%20the%20one%20that%20fits%20REPO%2C%0Afollowed%20by%20%60git%20push%20exe%20main%60%2C%20and%20say%20what%20you%20adapted%2C%20if%20anything.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open%20and%20these%20commands%2C%20which%20only%20I%20can%20run%2C%20from%20my%20own%20computer%20%28with%20this%20VM%27s%20name%20in%20them%29%3A%0A%20%20git%20clone%20-o%20exe%20my-django.exe.xyz%3Aapp.git%20my-django%20%20%23%20if%20you%20started%20a%20new%20project%0A%20%20git%20remote%20add%20exe%20my-django.exe.xyz%3Aapp.git%20%20%20%20%20%20%20%20%20%20%23%20or%2C%20in%20your%20own%20repository%27s%20checkout%0A",
    "terminal": "curl -fsSL https://exe-staging.dev/deploy/django/prompt.txt | ssh exe.dev new --prompt=/dev/stdin",
    "prompt": "REPO=\u003cfill-this-in\u003e\n\nSet up Django on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/django.md from top to bottom.\n\nThe values at the top of this message are mine:\n- REPO: the GitHub repository to deploy, as owner/name or a URL, or `new` to start a fresh Django app\nAsk me for any that still say \u003cfill-this-in\u003e before you need them.\n\nThe recipe's example VM name is my-django. This VM's name is the output of `hostname`: use it wherever the recipe writes my-django. Skip the step that creates the VM; you are on it.\n\nYou are done when all of these are true. Check each one yourself:\n- Your Django app, or a new Django 6.1 project, served by gunicorn behind nginx on port 8000, under systemd and enabled to start after a reboot.\n- `git push exe main` deploys: the pre-receive hook in `~/app.git` builds and migrates each push in a new `~/releases` directory and switches `~/app` to it only if every step passes, so a failing build rejects the push and the running release stays.\n- Production settings for exe.dev's proxy in `~/app-settings` and `~/app.env`, with DEBUG off, a generated secret key, HTTPS detected, and allowed hosts and CSRF origins set; `manage check --deploy` reports no issues.\n- SQLite in WAL mode at `~/data/db.sqlite3`, and uploads in `~/data/media` served at `/media/`.\n- The owner's exe.dev account is a Django superuser and opens `/admin/` without a password.\n- Nightly backups in `~/backups`, with a first one taken and checked.\n\nRead the whole recipe, including the closed details, and follow \"Set it\nup by hand\" in order, skipping \"Create the VM\"; the walkthrough above it\nis for the owner. REPO=new takes \"Start a new project\"; anything else\ntakes \"Bring your repository\". If cloning from GitHub fails, the\nrepository is private: give the owner the `ssh exe.dev integrations add\ngithub` command from that step, with this VM's name and the repository,\nwait for them, and clone from github.int.exe.xyz.\nNever overwrite an existing ~/app.env. Don't commit to the owner's code,\nand don't push test commits to ~/app.git: read the hook to check it.\nIf their repository doesn't fit the deploy hook (manage.py not at the\ntop, Pipenv, a custom user model without a username field), adapt the\nhook or the command and tell the owner what you changed.\nLeave the VM private and don't reboot: check that app.service, nginx and\napp-backup.timer are enabled instead. Skip the later Restore and Update\ninstructions; the restore check in setup uses a scratch copy only.\nThe URLs to give are https://\u003cvm\u003e.exe.xyz/ and /admin/. Of the two\ncommands at the end, give the owner only the one that fits REPO,\nfollowed by `git push exe main`, and say what you adapted, if anything.\n\nFinish with the URLs I should open and these commands, which only I can run, from my own computer (with this VM's name in them):\n  git clone -o exe my-django.exe.xyz:app.git my-django  # if you started a new project\n  git remote add exe my-django.exe.xyz:app.git          # or, in your own repository's checkout\n",
    "prompt_url": "https://exe-staging.dev/deploy/django/prompt.txt"
  },
  "updated": "2026-10-17",
  "related": [
    "fastapi",
    "postgres",
    "migrate-heroku",
    "claude-code",
    "docker-compose",
    "laravel"
  ],
  "alternatives": [
    "Heroku",
    "Railway",
    "Render",
    "PythonAnywhere"
  ]
}
