Build a production-ready Django web app in 7 steps—models, auth, admin, deployment. Written for solo founders who ship fast without boilerplate.
Django is a Python web framework that enables you to ship a production-ready web app in days, not months. You write less boilerplate, get an admin panel out of the box, and deploy to platforms that support WSGI without custom infrastructure. Need to validate an idea fast? Here's the thing: Django gets you there.
Photo: Ferenc Almasi on Unsplash
Who this is for: Solo founders and indie hackers who know Python basics and want to build data-driven web apps—like SaaS tools, internal dashboards, or membership sites—without reinventing authentication, routing, or database migrations. Even if you're new to Django but can read Python, you'll follow along.
Step 1: Install Django and Start a New Project
Django requires Python 3.8 or later. Create a virtual environment to isolate dependencies:
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install django
Start a new Django project:
django-admin startproject myapp
cd myapp
python manage.py runserver
Check http://127.0.0.1:8000/ in your browser. See Django's welcome page? That's your installation confirmed.
Django organizes projects by separating configuration (settings.py, urls.py) from app logic. A "project" holds global settings; "apps" contain features. Running multiple apps inside one project is useful if you're building a SaaS with distinct modules.
Step 2: Create Your First App and Define Models
Photo: Florian Olivo on Unsplash
Django apps are reusable modules. Create one:
python manage.py startapp core
Open core/models.py and define a data model. Here's a simple Task model for a to-do app:
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
Register the app in myapp/settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core', # Add this line
]
Run migrations to create the database table:
python manage.py makemigrations
python manage.py migrate
Django uses an ORM (Object-Relational Mapping) that translates Python classes into SQL. You don't write CREATE TABLE statements. Migrations are versioned, so you can roll back schema changes—critical when you're iterating fast and breaking things.
Step 3: Set Up Django Admin and Test CRUD Operations
Django's admin interface is production-ready. Create a superuser:
python manage.py createsuperuser
Follow the prompts. Then register your model in core/admin.py:
from django.contrib import admin
from .models import Task
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ('title', 'completed', 'created_at')
list_filter = ('completed',)
search_fields = ('title',)
Run the server and visit http://127.0.0.1:8000/admin/. Log in with your superuser credentials. You can now create, edit, and delete tasks without writing a single line of frontend code.
Honestly, Django admin can be enough for internal tools that don't require a custom UI. If you're building a B2B SaaS with power users, the admin panel can validate your idea before investing in React or Vue. For more insights on tools for indie hackers, check out our article on the Best Code Deployment Tools for Indie Hackers in 2026.
Step 4: Build Views and URL Routing
Views handle HTTP requests and return responses. Django supports function-based views (FBVs) and class-based views (CBVs). CBVs reduce repetition for common patterns like list/detail pages.
Create a list view in core/views.py:
from django.views.generic import ListView
from .models import Task
class TaskListView(ListView):
model = Task
template_name = 'core/task_list.html'
context_object_name = 'tasks'
ordering = ['-created_at']
Wire it up in myapp/urls.py:
from django.contrib import admin
from django.urls import path
from core.views import TaskListView
urlpatterns = [
path('admin/', admin.site.urls),
path('', TaskListView.as_view(), name='task-list'),
]
Create a template directory: core/templates/core/task_list.html:
<!DOCTYPE html>
<html>
<head>
<title>Tasks</title>
</head>
<body>
<h1>My Tasks</h1>
<ul>
{% for task in tasks %}
<li>
{{ task.title }}
{% if task.completed %}✓{% endif %}
</li>
{% empty %}
<li>No tasks yet.</li>
{% endfor %}
</ul>
</body>
</html>
Visit http://127.0.0.1:8000/. You'll see your task list. While Django's template engine may lack the power of Jinja2, it's sufficient for server-rendered HTML. Building an API-first app? Maybe skip templates and use Django REST Framework.
Step 5: Add User Authentication
Django includes a full authentication system. Enable login/logout views by adding to myapp/urls.py:
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', TaskListView.as_view(), name='task-list'),
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
Create core/templates/core/login.html:
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
</body>
</html>
Update myapp/settings.py to set login redirect:
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
Protect views with @login_required:
from django.contrib.auth.mixins import LoginRequiredMixin
class TaskListView(LoginRequiredMixin, ListView):
model = Task
template_name = 'core/task_list.html'
context_object_name = 'tasks'
ordering = ['-created_at']
login_url = '/login/'
Now unauthenticated users get redirected to login. Django's auth system handles password hashing, session management, and permissions. No need for Auth0 or Firebase Auth for most solo projects.
According to the Django documentation, the built-in User model supports custom fields via AbstractUser or AbstractBaseUser if you need email-only login or multi-tenancy.
Step 6: Connect a Production Database and Configure Settings
Django defaults to SQLite, which is fine for prototyping but risky in production. PostgreSQL is the standard choice for Django apps.
Install the PostgreSQL adapter:
pip install psycopg2-binary
Update myapp/settings.py:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myapp_db',
'USER': 'myapp_user',
'PASSWORD': 'secure_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
For production, use environment variables. Install python-decouple:
pip install python-decouple
Update settings:
from decouple import config
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default='5432'),
}
}
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
Create a .env file:
SECRET_KEY=your-secret-key-here
DEBUG=True
DB_NAME=myapp_db
DB_USER=myapp_user
DB_PASSWORD=secure_password
DB_HOST=localhost
DB_PORT=5432
Never commit .env to version control. Add it to .gitignore.
Django's settings module is Python code, so you can import libraries, set conditionals, and organize config however you want. Some developers split settings into base.py, dev.py, and prod.py. Worth noting: keeping one file with environment variables can reduce confusion.
Step 7: Deploy to Production
Django apps run on any WSGI server. Popular options for solo founders:
- Render: Free tier, deploys from GitHub, managed PostgreSQL.
- Railway: Simple pricing, built-in databases, one-command deploys.
- DigitalOcean App Platform: $5/month starter tier, scales with usage.
I'll show Render because it's the fastest path to a live URL.
Create a requirements.txt:
pip freeze > requirements.txt
Add gunicorn (WSGI server):
pip install gunicorn
pip freeze > requirements.txt
Create a render.yaml in your project root:
services:
- type: web
name: myapp
env: python
buildCommand: pip install -r requirements.txt && python manage.py migrate
startCommand: gunicorn myapp.wsgi:application
envVars:
- key: SECRET_KEY
generateValue: true
- key: DEBUG
value: False
- key: DATABASE_URL
fromDatabase:
name: myapp-db
property: connectionString
databases:
- name: myapp-db
databaseName: myapp_db
user: myapp_user
Push your code to GitHub. Connect your repo to Render. It auto-detects Django, provisions a PostgreSQL instance, and deploys.
Update myapp/settings.py to parse DATABASE_URL:
pip install dj-database-url
import dj_database_url
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL')
)
}
Set ALLOWED_HOSTS:
ALLOWED_HOSTS = [config('ALLOWED_HOSTS', default='localhost')]
In Render's dashboard, add ALLOWED_HOSTS=your-app.onrender.com. Redeploy. Your app is live.
According to Render's documentation, their platform handles SSL certificates, automatic scaling, and zero-downtime deploys. No need to mess with Nginx configs or systemd services.
What Nobody Tells You About Django
The ORM is not magic. Django generates SQL for you, but inefficient queries can still happen. Use select_related() and prefetch_related() to avoid N+1 problems. Production apps making 300+ database queries per page view? Yes, that happens when developers assume the ORM optimizes everything.
Migrations break in ways the docs don't warn about. Modifying a model, running makemigrations, then changing your mind and editing the migration file manually? That can mess up Django's migration graph. Always run migrate immediately after makemigrations in development. Test migrations on a staging database first in production.
Django's async support is incomplete. Django 3.1 added ASGI support, but most third-party packages assume WSGI. Need WebSockets or long-running tasks? Use Celery with Redis or RabbitMQ. Running async views in production without load testing? Be prepared—apps might crash due to non-async-safe middleware.
The admin panel has limits. Customizing complex workflows in the admin requires overriding templates and writing admin actions. Need a polished UI for non-technical users? Build a real frontend. The admin is best suited for internal tools and power users who can tolerate less polished interfaces.
Django REST Framework is a separate learning curve. DRF is the standard for building APIs, but it introduces serializers, viewsets, and router concepts. New to Django? Build a simple CRUD app with templates first. Adding DRF without understanding Django's ORM or request/response cycle might confuse you.
Common Mistakes
Skipping environment variables in development. Developers hardcode DEBUG=True and SECRET_KEY in settings.py, then forget to change them before deploying. Use .env files from day one. Five minutes of setup prevents production disasters.
Ignoring database indexes. Django doesn't auto-index foreign keys on all databases. If filtering by a field frequently—like Task.objects.filter(user=request.user)—add db_index=True:
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True)
Not using get_object_or_404. Beginners write:
task = Task.objects.get(pk=task_id)
This raises DoesNotExist if the object is missing, which crashes the app. Instead, use:
from django.shortcuts import get_object_or_404
task = get_object_or_404(Task, pk=task_id)
It returns a 404 response automatically.
Mixing business logic in views. Views should handle HTTP logic only—validation, redirects, context. Move calculations, external API calls, and data processing into model methods or service modules. Fat models, thin views.
FAQ
Can I use Django for a single-page app with React or Vue?
Yes. Django becomes your API backend. Install Django REST Framework, build serializers for your models, and expose endpoints. Your frontend consumes JSON. Honestly, this is how most projects ship—Django for auth and database logic, React for the UI.
How do I handle file uploads in Django?
Use FileField or ImageField in your model:
avatar = models.ImageField(upload_to='avatars/')
Django saves files to MEDIA_ROOT by default. In production, use S3 or Cloudflare R2 with django-storages. Serving files from your app server is slow and wastes memory.
Is Django slower than Flask or FastAPI?
For most solo projects, the difference doesn't matter. Django includes more batteries—ORM, admin, auth—so there's some overhead. Building a high-traffic API with sub-10ms response times? FastAPI is faster. Building a SaaS with forms, user accounts, and a database? Django ships quicker because you write less code.
How do I schedule background tasks in Django?
Use Celery with Redis. Install Celery:
pip install celery redis
Configure it in myapp/celery.py, then define tasks:
from celery import shared_task
@shared_task
def send_email(user_id):
# Email logic here
pass
Call it with .delay():
send_email.delay(user.id)
This runs the task asynchronously. Don't run long tasks in Django views—users wait for the response, and your app hangs.
Conclusion
Django trades flexibility for speed. You get authentication, migrations, and an admin panel without configuration. Validating a SaaS idea and need to ship in days? Django is the right tool.
Your next step: clone the starter repo or scaffold a new project, define one model, and deploy it to Render or Railway tonight. Stop reading tutorials and ship something live. Learn more in production than in 50 articles. For additional resources, consider exploring the Best Online Payment Solutions for Indie Hackers in 2026.
Editorial note: This article was produced with AI assistance and reviewed by Javier Valencia. Verified facts are distinguished from editorial opinion throughout the text. External sources linked are independent of NewsTide.
Sources
🇪🇸 Also available in Spanish: Leer en español