[Blog](https://console.f1b10.testing.getox.in/blog) [Guides](https://console.f1b10.testing.getox.in/blog#c-guides) # Deploy Django Without Docker: Gunicorn, systemd and Caddy Deploy Django without Docker on your Ubuntu VPS: gunicorn under systemd, Caddy for HTTPS and PostgreSQL. Every step by hand, then the same deploy with ox. Saurav Sharma Oct 8, 2026 9 min read On this page - [What you need](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-you-need) - [The pieces of a Django deploy without Docker](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-the-pieces) - [Deploy Django by hand, step by step](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-by-hand) - [What the manual setup leaves to you](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-is-left) - [The same deploy with ox](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-with-ox) - [Manual setup or ox: what each one gives you](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-manual-vs-ox) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#faq) - [Next step](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-next-step) You can deploy Django without Docker by running it as a plain process on an Ubuntu server: gunicorn serves the app, systemd keeps it running, Caddy handles HTTPS, and PostgreSQL holds the data. Ubuntu already ships every one of those pieces, so there is no image to build and no container runtime to babysit. This guide does it by hand first, so you know what every piece does. Then it shows the same deploy with ox, which sets up those same pieces on your server for you. **In this guide:** - [What you need](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-you-need) - [The pieces of a Django deploy without Docker](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-the-pieces) - [Deploy Django by hand, step by step](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-by-hand) - [What the manual setup leaves to you](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-is-left) - [The same deploy with ox](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-with-ox) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#faq) ## What you need - A VPS running the latest Ubuntu LTS. Hetzner, DigitalOcean, Vultr and Linode rent them for about $5 to $24 a month. - A domain name, with an A record pointing at the server's IP address. - A Django project in a Git repository, with its dependencies in `uv.lock` or `requirements.txt`, and `gunicorn` (or `uvicorn` for ASGI) among them. ## The pieces of a Django deploy without Docker | Piece | Job | | --- | --- | | gunicorn | Runs your Django app (WSGI) on a local port, with a few worker processes. | | systemd | Starts gunicorn at boot, restarts it if it crashes, and keeps its logs in the journal. | | Caddy | Answers on ports 80 and 443, gets and renews the HTTPS certificate, serves static files, and passes the rest to gunicorn. | | PostgreSQL | The database, on the same server, reachable only from it. | Nginx and Certbot can replace Caddy. Caddy is used here because it handles certificates on its own and its config is a few lines. If you prefer Nginx, Supervisor and Uvicorn, the [beginner's guide to Django with Uvicorn and Nginx](https://console.f1b10.testing.getox.in/blog/comprehensive-django-deployment-guide-for-beginners) walks through that stack. ## Deploy Django by hand, step by step ### 1. A user for the app, and the code Run the app as its own user, never as root. Shell ```bash sudo adduser --system --group --home /srv/myapp myapp sudo chmod 755 /srv/myapp sudo -u myapp git clone https://github.com/you/myapp.git /srv/myapp/app ``` The `chmod` lets Caddy read the static files later. A private repository needs a deploy key or a token for that clone. ### 2. Python and dependencies uv installs Python and your packages in one step. Shell ```bash sudo -u myapp -H sh -c 'curl -LsSf https://astral.sh/uv/install.sh | sh' cd /srv/myapp/app sudo -u myapp -H ~myapp/.local/bin/uv sync --frozen --no-dev ``` With `requirements.txt` instead, make a virtual environment and run `uv pip install -r requirements.txt` in it. ### 3. PostgreSQL Shell ```bash sudo apt install -y postgresql sudo -u postgres createuser myapp --pwprompt sudo -u postgres createdb -O myapp myapp ``` ### 4. Production settings Read secrets from the environment, never from the repository. Put them in a file only root and the app can read: Shell ```bash # /etc/myapp.env (chmod 640, owned by root:myapp) SECRET_KEY=a-long-random-string DATABASE_URL=postgresql://myapp:the-password@127.0.0.1:5432/myapp ALLOWED_HOSTS=example.com DEBUG=false ``` In `settings.py`, read them: Python ```python import os import dj_database_url SECRET_KEY = os.environ["SECRET_KEY"] DEBUG = os.environ.get("DEBUG") == "true" ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",") CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS] DATABASES = {"default": dj_database_url.config(conn_max_age=600)} STATIC_ROOT = BASE_DIR / "staticfiles" ``` Django's own `python manage.py check --deploy` lists the settings still missing for production, and the official [Django deployment checklist](https://docs.djangoproject.com/en/stable/howto/deployment/checklist/) explains each one. ### 5. Migrate and collect static files Shell ```bash cd /srv/myapp/app sudo -u myapp -H sh -c 'set -a; . /etc/myapp.env; set +a; \ ~/.local/bin/uv run python manage.py migrate --noinput && \ ~/.local/bin/uv run python manage.py collectstatic --noinput' ``` ### 6. A systemd service for gunicorn INI ```ini # /etc/systemd/system/myapp.service [Unit] Description=myapp (gunicorn) After=network.target postgresql.service [Service] User=myapp Group=myapp WorkingDirectory=/srv/myapp/app EnvironmentFile=/etc/myapp.env ExecStart=/srv/myapp/app/.venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3 Restart=always [Install] WantedBy=multi-user.target ``` Replace `config` with your project's package, the folder that holds `wsgi.py`. Then start it: Shell ```bash sudo systemctl daemon-reload sudo systemctl enable --now myapp journalctl -u myapp -f ``` ### 7. Caddy in front, with HTTPS Install Caddy from its official package (the [Caddy install docs](https://caddyserver.com/docs/install#debian-ubuntu-raspbian) list the apt commands for Ubuntu), then: Code ``` # /etc/caddy/Caddyfile example.com { handle_path /static/* { root * /srv/myapp/app/staticfiles file_server } reverse_proxy 127.0.0.1:8000 } ``` Run `sudo systemctl reload caddy`. Caddy gets a certificate for `example.com` on the first request. Open only SSH, 80 and 443 in the firewall: Shell ```bash sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw enable ``` ## What the manual setup leaves to you Your site is live. Here is what the setup above does not do yet: - **Updates without downtime.** A `git pull` and `systemctl restart` drops requests while gunicorn restarts, and a broken commit takes the site down. Doing it safely takes a second copy of the app and a switch; see [zero-downtime deploys with systemd and Caddy](https://console.f1b10.testing.getox.in/blog/zero-downtime-deploys-systemd-caddy). - **Backups.** Nothing saves the database yet. You need a daily `pg_dump`, a copy off the server, and a restore you have tried once. - **Going back.** When a release misbehaves, you need the previous one ready to run, not just its commit. - **Deploy on push.** A GitHub Action over SSH, or a webhook script, with the secrets that come with it. - **A place to test.** A staging copy with its own database, so you never try a migration on real data. ## The same deploy with ox ox sets up the same pieces on your server: your app runs as a systemd service under its own user, Caddy serves HTTPS, and PostgreSQL holds the data. There is still no Docker. The control panel is hosted by ox, so you install nothing but a small agent on the server. ### The steps 1. **Sign in with GitHub** at deploywithox.com. 2. **Add your server.** ox shows one command to paste on a fresh Ubuntu LTS server. It installs the agent and Caddy and locks the server down: a firewall that allows only SSH, 80 and 443, fail2ban, automatic security updates, and no password logins. 3. **Pick the repository.** ox reads it and shows the plan before anything runs. 4. **Set your variables**, like `SECRET_KEY`, then deploy. The [ox quickstart](https://console.f1b10.testing.getox.in/docs/quickstart) shows each of these screens. ### What ox detects from a Django repo From `manage.py` and your lockfile, with no config: - The install: `uv sync --frozen --no-dev` with `uv.lock`, or a virtual environment and `requirements.txt`. - The start: gunicorn on your project's `wsgi`, or uvicorn on `asgi` when uvicorn is a dependency and `asgi.py` exists. - The migration: `manage.py migrate --noinput`, run before visitors move to the new release. - `collectstatic --noinput`, when `settings.py` sets `STATIC_ROOT`. - PostgreSQL, when `psycopg` or `psycopg2` is a dependency and the repo has no `ox.toml` yet. ### A small ox.toml To set the domain and let Caddy serve your collected static files, add `ox.toml` at the repository root: TOML ```toml domains = ["example.com"] [static] paths = { "/static" = "staticfiles" } [services] postgres = {} ``` Once a repo has an `ox.toml`, the services are exactly the ones it lists, so `postgres` is declared here. ox provides `DATABASE_URL`, `PUBLIC_URL` and `PUBLIC_HOST`. You set `SECRET_KEY`, and `ALLOWED_HOSTS` can point at ox's value as `${PUBLIC_HOST}`. Keys in your `.env.example` that need your answer (empty, a secret, or a placeholder) are required, and a deploy waits until each one is set. Run `ox check` in the repo to see the whole plan offline. The [config reference](https://console.f1b10.testing.getox.in/docs/config#detection) lists every key and default. ### After the first deploy - Every push to your production branch deploys. The new release starts beside the old one and visitors move over only after it answers. - The database is backed up every day, kept on the server, and copied to your own S3-compatible bucket if you add one. ox also takes a snapshot before each migration. [How ox backs up and restores PostgreSQL](https://console.f1b10.testing.getox.in/docs/operate/backups) covers how many it keeps and how to restore one. - Rollback goes back to a kept release without rebuilding it. - A staging copy, and a preview for each branch once you turn previews on, each get their own database. ### Celery and scheduled jobs A Celery worker, beat, and cron jobs go in the same file: TOML ```toml [workers] worker = "uv run celery -A config worker --loglevel=INFO" [cron] digest = { schedule = "0 7 * * *", run = "uv run python manage.py send_digest" } [services] postgres = {} redis = {} ``` ox provides `REDIS_URL` for the broker. The [Django deploy guide](https://console.f1b10.testing.getox.in/docs/guides/django) has a complete `ox.toml` with a health check view, uploads kept across releases, and a daily cron job. ## Manual setup or ox: what each one gives you | Task | By hand | With ox | | --- | --- | --- | | HTTPS certificate | Caddy, once you write the Caddyfile | Caddy, set up for you | | Restart on crash and at boot | Your systemd unit | A systemd unit ox writes | | Deploy on git push | A GitHub Action or webhook script you maintain | Built in | | Updates without dropped requests | A second copy of the app and a switch you build | Built in, after a health check | | Daily database backups | A cron job with `pg_dump` and an off-site copy | Built in, with optional S3 | | Rollback | Keep old releases and switch back yourself | One action, no rebuild | ## Key takeaways - Django does not need Docker in production: gunicorn, systemd, Caddy and PostgreSQL on Ubuntu are enough. - Keep secrets in an environment file the app user can read, and run `manage.py check --deploy` before going live. - The hard parts come after the first deploy: updates without downtime, backups, rollback and a staging copy. - ox sets up the same native pieces on your own server and adds those parts, with no containers. ## Frequently asked questions ### Do I need Docker to deploy Django? No. Django runs well as a plain Python process under systemd, behind a web server like Caddy or Nginx. Docker is one way to package an app, not a requirement. On a single Ubuntu server, a virtual environment, a systemd unit and a reverse proxy give you the same result with fewer moving parts. ### Should I use gunicorn or uvicorn for Django? Use gunicorn for a regular WSGI Django app; it is the most common and best documented choice. Use uvicorn when you rely on Django's async views, websockets through Channels, or another ASGI feature. ox picks uvicorn on its own when uvicorn is a dependency and your project has an `asgi.py` file. ### How big a VPS do I need for Django? A small Django site with PostgreSQL usually runs in 1 to 2 GB of memory. Add more if you run Celery workers, Redis, or several apps on one server. ox tests every release on servers with 4 GB, which leaves room for a few apps, a staging copy and branch previews. ### Where do uploaded media files go without Docker? Not in the release folder, because it is replaced on every deploy. By hand, point `MEDIA_ROOT` at a folder outside the code and let your web server read it. With ox, list the folder under `[storage] keep` and it survives every release, or store uploads in an S3 bucket instead. ### Can I stop using ox later? Yes. The server is yours, your app is a plain program on it, and your database is plain PostgreSQL that you can dump at any time. Running `sudo ox uninstall` removes what ox added and keeps the security settings in place, so the server stays locked down after you leave. ## Next step Try the manual steps on a spare server once, so you know what each piece does. When you would rather not maintain the deploy script, the backups and the rollback yourself, follow the [Django guide in the ox docs](https://console.f1b10.testing.getox.in/docs/guides/django) and deploy the same app with ox, free for your first project. New to running apps on your own server? [How ox self-hosts your apps](https://console.f1b10.testing.getox.in/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-you-need) - [The pieces of a Django deploy without Docker](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-the-pieces) - [Deploy Django by hand, step by step](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-by-hand) - [1. A user for the app, and the code](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-user-and-code) - [2. Python and dependencies](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-python) - [3. PostgreSQL](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-postgres) - [4. Production settings](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-settings) - [5. Migrate and collect static files](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-migrate) - [6. A systemd service for gunicorn](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-systemd) - [7. Caddy in front, with HTTPS](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-caddy) - [What the manual setup leaves to you](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-what-is-left) - [The same deploy with ox](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-with-ox) - [The steps](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-ox-steps) - [What ox detects from a Django repo](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-detected) - [A small ox.toml](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-ox-toml) - [After the first deploy](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-after) - [Celery and scheduled jobs](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-celery) - [Manual setup or ox: what each one gives you](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-manual-vs-ox) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#faq) - [Do I need Docker to deploy Django?](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-faq-docker) - [Should I use gunicorn or uvicorn for Django?](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-faq-gunicorn) - [How big a VPS do I need for Django?](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-faq-size) - [Where do uploaded media files go without Docker?](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-faq-media) - [Can I stop using ox later?](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-faq-leave) - [Next step](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker#heading-next-step) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post A Heroku alternative on your own VPS, without Docker](https://console.f1b10.testing.getox.in/blog/heroku-alternative-on-your-own-server) [Newer post A FastAPI App on Your Own Server with One Config File](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps) ## Keep reading - Guides [How to Self-Host Web Apps on Your Own VPS (2026 Guide)](https://console.f1b10.testing.getox.in/blog/self-host-web-apps-on-your-own-vps) What you need to self-host a web app on a VPS: server, domain, HTTPS, database, backups. Four ways to set it up, their trade-offs, and how ox does it. Saurav Sharma Oct 15, 2026 8 min read - Guides [A FastAPI App on Your Own Server with One Config File](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps) Put a FastAPI app with uvicorn, Postgres and Alembic on your own Ubuntu server, with HTTPS and no Docker, using one small ox.toml file you can copy. Saurav Sharma Oct 8, 2026 7 min read - Guides [Deploy Django and Vue.js on a VPS: Nginx, Gunicorn, Celery](https://console.f1b10.testing.getox.in/blog/deployment-guide-for-django-vuejs-project-on-vps) Deploy a Django and Vue.js app on a VPS: Nginx serves the Vue build, Gunicorn runs Django, Celery and Redis under systemd, with HTTPS from Certbot. Saurav Sharma Jun 4, 2025 14 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://console.f1b10.testing.getox.in/login/github) [Read the docs](https://console.f1b10.testing.getox.in/docs)