Django (Server Only) starter template, inherited from SaaS Pegasus.
This project runs in two distinct modes:
| Mode | How it runs | Database | Cache / broker | Celery | Used for |
|---|---|---|---|---|---|
| Local | Natively on your machine via uv + npm |
SQLite (db.sqlite3) |
DummyCache (no Redis needed) | Eager (synchronous) | Day-to-day development |
| Production | Docker Compose (Postgres + Redis + web + Vite + Celery) | Postgres | Redis | Real worker + beat | Containerized / deployed stack |
The split is driven by DEBUG and a few environment variables — see Configuration. You do
not need Docker for everyday development.
- Local development
- Everyday commands
- Database
- Front end
- Generating the API client
- Celery & background tasks
- Testing
- Code quality & git hooks
- Configuration
- Production
Local development runs natively on your Python environment (managed with uv) against a SQLite database. No Docker, Postgres, or Redis required.
Install these once on your machine:
- uv — manages the virtual environment
and provisions Python 3.14 for you (you do not need to install Python 3.14 separately):
curl -LsSf https://astral.sh/uv/install.sh | sh # or: brew install uv
- Node.js 20+ — provides
nodeandnpmfor the front-end build. make— preinstalled on macOS/Linux. On Windows, follow these instructions.
From the project root:
make initThis single command:
- Copies
.env.example→.env(only if.envdoesn't already exist). - Installs Python dependencies into a local
.venvwithuv sync. - Installs front-end dependencies with
npm install. - Creates the SQLite database (
db.sqlite3) and applies all migrations.
In DEBUG, make start (i.e. runserver) automatically creates a development superuser the
first time it runs, then silently skips it afterwards. Defaults:
| Field | Value |
|---|---|
admin@example.com |
|
| Password | admin |
Override via DEV_SUPERUSER_EMAIL / DEV_SUPERUSER_PASSWORD in .env. This only happens when
DEBUG is true, so production is never affected. You can still create more accounts with
make manage ARGS='createsuperuser'.
Local development needs two terminals running simultaneously — the Django server and the Vite
front-end server. In DEBUG, CSS/JS are served by Vite, so if you skip the second process the page
loads unstyled.
Terminal 1 — Django backend (port 8000):
make startTerminal 2 — Vite front end (port 5173):
make npm-devVisit http://localhost:8000 — you should see a fully styled page. The Django admin is at http://localhost:8000/admin/ (log in with the auto-created dev superuser from step 2).
Styles not loading? It almost always means the Vite dev server (step 3, Terminal 2) isn't running, or
node_modulesis missing. Runmake npm-installthenmake npm-devand refresh.
New to Pegasus? Try these next steps.
Run make with no arguments to list every available target. Local targets (start, migrate,
test, shell, …) run natively via uv; production targets are prefixed with prod- and use Docker.
| Command | What it does |
|---|---|
make start |
Run the Django dev server |
make shell |
Open a Django Python shell |
make dbshell |
Open a database shell (SQLite locally) |
make manage ARGS='<cmd>' |
Run any manage.py command, e.g. ARGS='createsuperuser' |
make migrations |
Create new migrations |
make migrate |
Apply migrations |
make test |
Run the test suite |
make ruff |
Format and lint Python with Ruff |
make npm-dev |
Run the Vite dev server |
make npm-build |
Build production front-end assets |
Management commands can also be run directly: uv run python manage.py <command>.
Local development uses SQLite by default — no setup required. Leaving DATABASE_URL unset makes
Django use the db.sqlite3 file in the project root.
make migrations # uv run python manage.py makemigrations
make migrate # uv run python manage.py migrateIn production, set DATABASE_URL (e.g. a Postgres connection string) in the environment — it takes
precedence over SQLite automatically.
JavaScript/TypeScript lives in assets/ and is bundled by Vite and served
through django-vite. Tailwind v4 + DaisyUI provide styling.
make npm-install # install all npm packages
make npm-install <package> # install a specific package
make npm-dev # Vite dev server with hot reload (local development)
make npm-build # build optimized assets (production)
make npm-type-check # TypeScript type checkingIn DEBUG, DJANGO_VITE["default"]["dev_mode"] is True, so assets are served live from the Vite
dev server. With DEBUG=False, Django serves the built manifest from npm-build instead.
The REST API (Django Rest Framework) publishes an OpenAPI schema via drf-spectacular at
/api/schema/. The TypeScript client in api-client/ is generated from that schema and consumed
by the front end (see assets/javascript/api.js). It is generated code — don't edit it by hand.
Whenever you add or change API endpoints, regenerate the client:
make generate-api-clientThis target:
- Exports the OpenAPI schema from Django (
manage.py spectacular). - Runs OpenAPI Generator (
typescript-fetch), pinned to the version inOPENAPI_GENERATOR_VERSION(kept in sync withapi-client/.openapi-generator/VERSION), vianpx @openapitools/openapi-generator-cli. - Writes the result into
api-client/and cleans up the temporary schema file.
Requirements: node/npx and a Java runtime (the generator runs on the JVM). Review the diff
before committing.
Celery runs background and scheduled tasks. Locally, tasks run eagerly (synchronously) by default
(CELERY_TASK_ALWAYS_EAGER defaults to DEBUG), so no broker is required.
To exercise the real worker locally, run a Redis instance, set REDIS_URL in .env, then:
make celery
# or directly:
uv run celery -A config worker -l INFO --beat --pool=soloThe
solopool is fine for development but not for production.
In production (DEBUG=False) tasks dispatch to the Redis broker and are processed by the dedicated
celery container (see Production).
make test # run everything
make test ARGS='apps.web.tests.test_basic_views' # a single module
make test ARGS='apps.web.tests.test_basic_views --keepdb' # reuse the test DB (faster)On Linux you can re-run tests on change:
find . -name '*.py' | entr uv run python manage.py test apps.web.tests.test_basic_viewsmake ruff # ruff format + lint --fix
make ruff-format # format only
make ruff-lint # lint + autofix only
make type-check # mypyInstall the pre-commit hooks (run automatically on every commit):
uv run pre-commit install --install-hooksSee the Pegasus code-formatting docs for details.
Configuration is read from environment variables (via .env locally). make init /
make setup-env copy .env.example → .env for you. The most important variables:
| Variable | Default | Purpose |
|---|---|---|
SECRET_KEY |
insecure dev key | Django secret key. Set a strong value in production. |
DEBUG |
True |
Debug mode. Must be False in production (or use config.settings.prod). |
ALLOWED_HOSTS |
* |
Comma-separated allowed hosts. Restrict in production. |
DATABASE_URL |
(unset → SQLite) | Postgres connection string in production. |
REDIS_URL |
(unset → redis://localhost:6379/0) |
Cache + Celery broker. Required in production. |
CELERY_TASK_ALWAYS_EAGER |
= DEBUG |
Run tasks synchronously when true. |
DEV_SUPERUSER_EMAIL / DEV_SUPERUSER_PASSWORD |
admin@example.com / admin |
Auto-created dev superuser (DEBUG only). |
ENABLE_DEBUG_TOOLBAR |
False¹ |
Django Debug Toolbar (disabled during tests). |
EMAIL_BACKEND |
console backend | Email backend; configure a real one (e.g. Mailgun/Anymail) in production. |
DJANGO_PORT / DJANGO_VITE_PORT |
8000 / 5173 |
Dev server ports. |
POSTGRES_PORT / REDIS_PORT |
5432 / 6379 |
Docker service ports. |
TURNSTILE_KEY / TURNSTILE_SECRET |
(empty) | Cloudflare Turnstile keys (optional). |
GOOGLE_ANALYTICS_ID |
(empty) | GA measurement ID (optional). |
¹ .env.example ships with ENABLE_DEBUG_TOOLBAR=True for convenience.
Never commit
.env— it's git-ignored. See.env.examplefor the full, annotated list.
Settings modules: live in the config/settings/ package.
config.settings.base— shared settings imported by both environment modules. Not selected directly.config.settings.dev— the default, used everywhere unless overridden.DEBUGdefaults toTrue.config.settings.prod— imports everything frombase, then forcesDEBUG=Falseand enables the security hardening (SSL redirect, secure cookies, HSTS scaffolding, etc.). Select it in production viaDJANGO_SETTINGS_MODULE=config.settings.prod.
Docker Compose runs a production-ready stack — Postgres, Redis, a gunicorn web server, and a
Celery worker (with beat). The web and Celery services share one image built from Dockerfile
(a multi-stage build that compiles the front-end assets with Vite, then installs the production
Python dependencies) and run with DJANGO_SETTINGS_MODULE=config.settings.prod.
Requirements: Docker and Docker Compose.
make setup-env-prod # copies .env.prod.example -> .env.prod (git-ignored)Edit .env.prod and set real values — at minimum SECRET_KEY, ALLOWED_HOSTS,
POSTGRES_PASSWORD / DATABASE_URL, and REDIS_URL. DEBUG=False is required (see the notes in
the file). For real email, configure EMAIL_BACKEND and its credentials (e.g. Mailgun via Anymail).
make prod-build # build the production image
make prod-start # start the stack (foreground)
make prod-start-bg # start the stack (background)
make prod-stop # stop the stack
make prod-restart # stop + start
make prod-ssh # shell into the running web container
make prod-manage ARGS='migrate' # run a manage.py command in the web containerOn startup the web service applies migrations and runs collectstatic automatically, then
serves the app with gunicorn on port 8000. Static files are served directly by the app via
WhiteNoise — no separate web server is required (put a
TLS-terminating reverse proxy in front for HTTPS; the production settings honour the
X-Forwarded-Proto header).
docker-compose.yml defines four services:
| Service | Image / build | Role |
|---|---|---|
db |
postgres:17 |
Postgres database (persisted in the postgres_data volume) |
redis |
redis:7 |
Cache + Celery broker (persisted in redis_data) |
web |
Dockerfile |
Django app under gunicorn on port 8000 (runs migrate + collectstatic on boot) |
celery |
Dockerfile |
Celery worker + beat |
Uploaded media persists in the media_files volume. MY_UID / MY_GID (in the Makefile) set the
container user/group so files created in mounted volumes belong to your host user rather than root.
The defaults (1000) suit most setups.
Validate the deployment settings:
make prod-manage ARGS='check --deploy'Consider also enabling HSTS (see the commented block in config/settings/prod.py) once you're
confident HTTPS works, and scaling GUNICORN_WORKERS in .env.prod to (2 × CPU cores) + 1.
Built with SaaS Pegasus, the Django SaaS boilerplate.