There is a point in building enough Django projects where starting a new one no longer feels new.
The product might be different, but the first few hours usually look strangely familiar.
Create the settings structure.
Configure environment variables.
Replace Django’s default user model before the first migration.
Set up PostgreSQL.
Add authentication.
Configure email.
Write the Dockerfile.
Set up linting, formatting, tests, and CI.
Then open the last project, copy the parts that worked, and hope you remembered everything.
None of this is particularly difficult.
That was actually the problem.
We were spending some of the most valuable early hours of a project solving problems we had already solved several times.
At Inflection Studio, a lot of our application and system development work involves getting from an idea to a reliable product relatively quickly. And after enough Django projects, it became obvious that we were rebuilding the same foundation each time.
So we stopped.
Instead, we turned the decisions we kept making into an open-source Django Cookiecutter.
It is essentially our preferred Django starting point: architecture, tooling, configuration, testing, and delivery checks packaged into a project generator that we can use repeatedly.
But the more interesting part is not that it saves us from having to create a few files.
It is what happens when the lessons from one project become the defaults for the next one.
Starting from scratch is not really starting from nothing
A blank Django project gives you flexibility.
It also gives you a surprisingly large number of decisions to make.
Where do your applications live?
How do you separate settings across local, test, staging, and production?
Do users authenticate with usernames or email addresses?
How should transactional email work?
Where does reusable framework code belong?
What should the Docker image contain?
Which checks need to pass before code can be merged?
All reasonable questions.
The problem starts when every project answers them differently.
One repository has a single settings file. Another has four.
One uses Poetry. Another uses pip with manually maintained requirements files.
One production Docker image contains compilers and package-management tooling. Another project does not use Docker at all.
All of them might work perfectly well on their own.
But over time, those differences create friction.
Engineers need to relearn repository conventions every time they switch projects. Onboarding takes longer. Pull requests end up debating structure instead of product behaviour. Dependency upgrades and infrastructure improvements have to be rediscovered in every codebase.
The real cost of starting from scratch is not the setup itself.
It is the inconsistency that compounds afterwards.
Standardisation does not mean making every product the same
This was probably the most important distinction for us.
I am generally wary of abstractions that become rigid too early.
A marketplace, an internal operations platform, and a content-heavy application should not have identical architectures simply because they were all built with Django.
The product should be allowed to shape the system.
But there are also decisions that rarely benefit from being reconsidered from first principles every time.
Environment handling.
Authentication conventions.
Dependency management.
Testing.
Container security.
CI expectations.
Those are not usually the places where a product’s uniqueness should live.
Key Idea
We are not standardising the product. We are standardising how confidently the product can begin.
| We standardise | The generated project can vary |
|---|---|
| Project layout and settings boundaries | Username or email authentication |
| Dependency, linting, and test tooling | Blog, tags, Django REST Framework, and editor support |
| CI expectations and Docker runtime design | Email provider, timezone, phone fields, and brand colours |
The choices that genuinely vary between products still remain configurable.
They are simply made explicit.
Cookiecutter gives us the mechanism for collecting those choices. The template is where our engineering opinions around them live.
A simplified version looks something like this:
{
"project_name": "My Awesome Project",
"username_type": ["username", "email"],
"mail_service": [
"Mailgun",
"Amazon SES",
"Mailjet",
"Mandrill",
"Postmark",
"SendGrid",
"Brevo",
"SparkPost",
"Other SMTP"
],
"has_blog": "n",
"has_tags": "n",
"use_docker": "n",
"use_drf": "n"
}
The answers determine what gets generated.
Simple idea.
Very useful once you have enough repeated decisions.
Turning experience into an executable standard
The template did not begin as an architecture exercise.
A lot of it came from looking back at patterns that had worked well across projects we had already built, including products such as Revlyn, and asking a fairly simple question:
What do we already know that we should not have to rediscover on the next project?
We also looked at where some of our older defaults had accumulated friction and compared our approach with the wider Django ecosystem.
One project we learned a great deal from was Cookiecutter Django.
Not only because of its technical decisions, but because of the discipline around testing, documentation, configuration, and maintaining an open-source project over time.
Our template is independently maintained and reflects how we prefer to build at Inflection Studio, but it would be dishonest to talk about it without acknowledging the work that helped shape some of our thinking.
Some ideas we kept.
Some we simplified.
Others changed because our own workflows had evolved.
The generated project structure, for example, separates product applications, common framework code, and environment-specific configuration:
project/
├── apps/
│ ├── login/
│ ├── dashboard/
│ └── blog/
├── common/
│ └── db/
├── conf/
│ └── settings/
│ ├── common.py
│ ├── local.py
│ ├── test.py
│ ├── staging.py
│ └── live.py
├── constants/
├── utils/
└── manage.py
Views and API handlers should stay thin.
Multi-step business workflows belong in services.
Third-party integrations should have an obvious home.
Shared database abstractions should not become tangled with application-specific logic.
There is nothing particularly revolutionary about any one of those decisions.
Their value comes from predictability.
When another engineer joins a project, they should have a reasonable idea of where something belongs before asking the person who originally created the repository.
That same thinking now extends to AI-assisted development.
The template generates an AGENTS.md file documenting architectural boundaries, testing expectations, migration safety, and the commands used to work with the repository.
As coding agents become increasingly normal in engineering workflows, I think repository conventions need to become legible to both humans and machines.
Important architectural knowledge should not live only in someone’s head.
Simplifying the Python workflow with uv
This project also gave us an excuse to revisit dependency management.
Both the Cookiecutter itself and the projects it generates now use uv with pyproject.toml, dependency groups, and a lock file.
Generated projects use a .venv/ at the project root.
The normal workflow is intentionally boring:
uv sync --locked
uv run python manage.py migrate
uv run pytest
That is more or less the point.
The post-generation hook also creates the project’s uv.lock, which means a freshly generated repository starts with a reproducible dependency state.
Speed is obviously one of uv’s advantages, but it was not the main reason we liked the move.
It also brings dependency management, virtual environments, Python installation, locking, and command execution into a single tool.
Local development, CI, and Docker can therefore work from the same dependency definition.
I generally do not think fewer tools automatically means better engineering.
But fewer competing ways of doing the same thing is usually helpful.
The current template targets Python 3.14 and Django 6.1.
Email should be infrastructure, not application logic
Transactional email is one of those decisions that feels permanent until it suddenly is not.
A product might start with Mailgun.
Later it moves to Amazon SES because of volume.
Another project might prefer Postmark.
The application itself should not care too much.
For supported providers, the template uses Anymail, which exposes multiple transactional email services through Django’s normal email API.
The provider selected during project generation becomes part of the generated configuration and environment requirements:
MAILERS = {
"default": {
"BACKEND": "anymail.backends.postmark.EmailBackend",
},
}
Projects that need conventional SMTP can still use Django’s SMTP backend.
Local development and tests use an in-memory backend so emails are never delivered externally by accident.
Again, the interesting part is not that the template gives you a long list of email providers.
It is that this decision is isolated from application behaviour.
If the infrastructure changes later, the product should not need to change with it.
Production Docker images should contain production things
Another area we revisited was Docker.
Our earlier setup worked.
But it also included more tooling in the production image than was necessary.
The current template uses a multi-stage build with separate base, builder, and runtime stages:
FROM ghcr.io/astral-sh/uv:0.12.6 AS uv
FROM python:3.14-slim AS base
FROM base AS builder
COPY --from=uv /uv /uvx /bin/
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev --no-install-project
FROM base AS runtime
COPY --from=builder /code/.venv /code/.venv
COPY . .
The builder can contain uv, compilers, development headers, and everything else needed to assemble the environment.
Production does not need most of that.
The runtime receives the completed virtual environment, application code, and the operating-system libraries required to run the application.
That is one of the main benefits of Docker multi-stage builds: the things required to build an application do not automatically have to be included in what you ship with it.
But there is another lesson here.
An architecture decision is only useful if you can keep verifying that it remains true.
So our CI builds the generated image, starts it, runs Django’s checks, imports the production dependencies, and verifies that tools such as uv and GCC are not present in the final container.
Smaller images are useful.
Knowing what is actually inside them is more useful.
Testing the project people actually receive
This turned out to be the part of the Cookiecutter that required the most deliberate thought.
It is quite easy to look at a template repository and convince yourself that it works.
The individual files can all look correct.
The generated project can still be broken.
A Jinja condition might leave invalid YAML behind.
Turning off a feature can leave an import pointing to a module that no longer exists.
A model can change without its migration being updated.
A Dockerfile can successfully build and then fail as soon as the application starts.
So we decided that testing the template itself was not enough.
We needed to test the thing it generates.
The test suite renders representative project combinations and inspects the resulting repositories.
It checks that:
- no unresolved Cookiecutter markers remain;
- generated Python can be parsed;
- YAML and TOML files remain valid;
- conditional files only exist when their corresponding features are enabled;
- the generated project passes Ruff;
- authentication modes produce the correct models and settings;
- every supported email provider produces the expected configuration;
- initial migrations exist for generated applications;
- dependency and Dependabot configuration matches the selected options.
CI then takes things further.
It generates a Docker-enabled project from scratch, installs the locked dependencies, and runs:
python manage.py check
python manage.py makemigrations --check --dry-run
pytest
It validates the development and staging Compose overlays.
It builds the production Docker image.
Then it runs Django inside that image.
<strong>Rendered, not assumed</strong>
<p>The generation suite inspects the files users receive, including conditional paths, syntax, migrations, and provider settings.</p>
<strong>Built, then booted</strong>
<p>The smoke job validates Compose, builds the production image, and runs Django inside the finished container.</p>
That distinction became quite important to me.
The question is not:
Does the template look correct?
The question is:
Can a project generated from this template actually start, test, and build?
Those are very different levels of confidence.
The biggest benefit comes after the project is generated
The obvious selling point of Cookiecutter is speed.
Run a command.
Answer a few questions.
A repository appears.
Useful, yes.
But I think that is probably the least interesting part.
The larger benefit shows up weeks and months later.
An engineer opening one of these projects already knows roughly where things live.
They already know the commands.
They know what CI expects.
They know how dependencies are managed.
They understand the settings boundaries.
Docker behaves predictably.
Pull requests can spend more time discussing product behaviour rather than negotiating repository conventions.
And when we improve the template, the next project automatically inherits that improvement.
That is where the value starts to compound.
A migration pattern learned the hard way once can become the default for every future project.
A safer Docker configuration is generated code rather than documentation someone needs to remember to read.
A CI check created because of one regression can prevent the same class of problem from reaching another project.
Over time, the template becomes something else.
It becomes a record of lessons we have already paid for.
Who this Django Cookiecutter is for
We built this primarily around how we work at Inflection Studio, but the problems it solves are not particularly unique to us.
It should be useful for teams that want a production-ready Django starting point without having to rebuild authentication, environment configuration, dependency management, CI, email, and Docker conventions every time.
Especially if:
- several engineers regularly move between Django repositories;
- your studio or product team wants consistent engineering defaults;
- projects frequently need optional API, Docker, blog, tagging, or email-provider support;
- you want the generated application itself tested before feature development starts.
This is not an argument that every Django project should use the same architecture.
I do not think they should.
It is simply a curated collection of defaults for teams whose priorities align with ours.
The trade-off with opinionated templates
Of course, there is no universal Django architecture.
Some teams will prefer completely different deployment models.
Different application boundaries.
Different frontend tooling.
Different infrastructure.
That is fine.
An opinionated template is useful because it makes decisions.
If we tried to support every possible Django architecture, the template would eventually have little to no opinion at all.
The more important trade-off is maintenance.
A starter project that slowly becomes stale can actually be worse than having no starter project.
It creates confidence in decisions that nobody is actively revisiting.
Dependencies change.
Django changes.
Docker practices change.
Providers disappear.
Things that were sensible three years ago can quietly become liabilities.
That is why I think the automation around the template matters almost as much as the template itself.
Pre-commit checks.
Generated-project tests.
Docker smoke tests.
Dependabot configuration.
Contribution guidance.
A security policy.
Those things might look like open-source housekeeping.
They are really part of keeping the starting point trustworthy.
Why we decided to open-source it
We could easily have kept this as an internal Inflection Studio tool.
There would have been nothing particularly wrong with that.
But Django itself, and much of the ecosystem we rely on when building software, exists because people decided to make their useful work publicly available.
This template also benefited from other open-source projects, libraries, documentation, and ideas.
Publishing ours felt like a small way of returning some of that value.
There is another benefit too.
Public code creates pressure.
Internal conventions can survive for a long time because the people who created them already understand the missing context.
When you publish something, the assumptions become visible.
The confusing parts become more obvious.
People can disagree with your decisions.
And occasionally someone shows you a much better way of doing something.
That is healthy.
We are not presenting this Cookiecutter as the final answer to starting a Django project.
It is simply the best version of our current thinking.
A working foundation shaped by real projects, mistakes, improvements, and the things we would rather not have to learn twice.
The template is available under the permissive BSD 3-Clause License.
Use it as-is.
Fork it and disagree with us.
Open an issue if something does not make sense.
Send a focused pull request if you have found a better pattern.
You can find the project on GitHub.
Once Cookiecutter and uv are installed, generating a project is one command:
uvx cookiecutter https://github.com/Inflection-Studio/django-projects-cookiecutter
On the surface, it creates a Django repository.
What we really wanted it to create was a shared starting position.
One that is tested, documented, predictable, and ready for the actual product work to begin.
Less repeated setup.
Less structural drift.
And more time spent building the thing that got us started in the first place.