Photo by ChatGPT
Welcome to the final post in the Django Bootcamp series! We’ve built a fully functional bookmark manager with CRUD operations, and now it’s time to add two finishing touches: user authentication and deployment.
This is a 5 part series where we’ll go from zero to a deployed Django web application. By the end, we’ll have built a bookmark manager — a practical app for saving, organizing, and tagging your favorite links. Here’s the full series outline:
- Getting Started with Python for Web Development — Python basics, pip, and virtual environments
- Creating Your First Django Project — project structure, the development server, and your first view
- Models & the Django Admin — defining your database models and using Django’s built-in admin panel
- Views & Templates — URL routing, views, templates, and building out the bookmark CRUD
- Authentication & Deployment (this post) — user login, protecting pages, and deploying to production
Let’s finish this thing!
Django’s Built-in Auth System
One of Django’s biggest strengths is that it comes with a complete authentication system out of the box. If you look at your INSTALLED_APPS in settings.py, you’ll see django.contrib.auth is already there. That means we have user models, login/logout views, password hashing, and session management ready to go — no extra packages needed.
Adding Auth URLs
Django provides pre-built views for login, logout, and password management. We just need to include them in our URL configuration. Open bookmarks_project/urls.py and update it:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("", include("bookmarks.urls")),
]
The django.contrib.auth.urls line gives us these URL patterns for free:
/accounts/login/— Log in/accounts/logout/— Log out/accounts/password_change/— Change password/accounts/password_reset/— Reset password
For our bookmark manager, we’ll focus on login and logout.
Creating the Login Template
Django’s built-in auth views handle all the logic, but they expect us to provide the templates. By default, the login view looks for a template at registration/login.html.
Create the directory and template:
mkdir -p bookmarks/templates/registration
Now create bookmarks/templates/registration/login.html:
{% extends "bookmarks/base.html" %}
{% block title %}Log In{% endblock %}
{% block content %}
<h1>Log In</h1>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div style="margin-bottom: 15px;">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
{% for error in field.errors %}
<p class="error">{{ error }}</p>
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="error">{{ error }}</p>
{% endfor %}
{% endif %}
<button type="submit" class="btn">Log In</button>
</form>
{% endblock %}
This looks very similar to our bookmark create form — that’s the beauty of Django’s consistent form handling. The form.non_field_errors section handles errors that aren’t tied to a specific field, like “Invalid username or password.”
Configuring Redirects
After a user logs in or logs out, Django needs to know where to send them. Add these lines to the bottom of bookmarks_project/settings.py:
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
Now when a user logs in, they’ll be redirected to the home page. Same thing when they log out.
Updating the Navigation
Let’s update our base template to show different links depending on whether the user is logged in or not. Open bookmarks/templates/bookmarks/base.html and replace the <nav> section:
<nav>
<a href="{% url 'home' %}">Home</a>
{% if user.is_authenticated %}
<a href="{% url 'bookmark_create' %}">Add Bookmark</a>
<a href="{% url 'logout' %}">Log Out ({{ user.username }})</a>
{% else %}
<a href="{% url 'login' %}">Log In</a>
{% endif %}
</nav>
Now logged-in users will see the “Add Bookmark” and “Log Out” links, while anonymous users will only see “Log In.” The {{ user.username }} part shows the logged-in user’s name next to the logout link, so they know which account they’re using.
Protecting Views with @login_required
Right now, anyone can create or delete bookmarks — even if they’re not logged in. Let’s fix that. Django provides a @login_required decorator that redirects anonymous users to the login page.
Update bookmarks/views.py:
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from .forms import BookmarkForm
from .models import Bookmark
def home(request):
bookmarks = Bookmark.objects.all()
return render(request, "bookmarks/home.html", {"bookmarks": bookmarks})
def bookmark_detail(request, pk):
bookmark = get_object_or_404(Bookmark, pk=pk)
return render(request, "bookmarks/detail.html", {"bookmark": bookmark})
@login_required
def bookmark_create(request):
if request.method == "POST":
form = BookmarkForm(request.POST)
if form.is_valid():
form.save()
return redirect("home")
else:
form = BookmarkForm()
return render(request, "bookmarks/create.html", {"form": form})
@login_required
def bookmark_delete(request, pk):
bookmark = get_object_or_404(Bookmark, pk=pk)
if request.method == "POST":
bookmark.delete()
return redirect("home")
return render(request, "bookmarks/delete.html", {"bookmark": bookmark})
Notice that home and bookmark_detail stay public — anyone can browse bookmarks. But bookmark_create and bookmark_delete are protected with @login_required. If an anonymous user tries to visit /bookmark/new/, they’ll be automatically redirected to the login page. After they log in, Django will send them right back to the page they were trying to reach. Pretty slick!
Testing the Auth Flow
Let’s make sure everything works. Start the server and walk through these steps:
- Visit the home page — You should see the “Log In” link in the navigation.
- Try to add a bookmark by navigating to
/bookmark/new/directly — You should be redirected to the login page. - Log in with the superuser account you created earlier.
- Check the navigation — You should now see “Add Bookmark” and “Log Out (yourusername).”
- Create a bookmark — It should work as before.
- Log out — You should be redirected to the home page, and the navigation should show “Log In” again.
If all of that works, our authentication system is good to go!
Preparing for Deployment
Our app works great locally, but we need to make some changes before deploying to production. Let’s install a few packages and update our settings.
Installing Dependencies
pip install gunicorn whitenoise dj-database-url psycopg2-binary
Here’s what each package does:
gunicorn— A production-grade WSGI server. The built-inrunservercommand is only for development.whitenoise— Serves static files (CSS, JavaScript, images) efficiently in production.dj-database-url— Lets you configure your database using a single URL string, which is how most hosting platforms provide database credentials.psycopg2-binary— A PostgreSQL adapter for Python. We’ll use PostgreSQL in production instead of SQLite.
Now freeze your dependencies:
pip freeze > requirements.txt
This creates a requirements.txt file that lists every package (and its version) that your project needs. Hosting platforms use this file to install dependencies when deploying your app.
Updating settings.py
We need to make several changes to bookmarks_project/settings.py to make it production-ready. Let’s go through each one.
First, add this import at the top of the file:
import os
import dj_database_url
Secret Key — Never expose your secret key in production. Replace the hardcoded key with:
SECRET_KEY = os.environ.get("SECRET_KEY", "your-default-dev-key-change-me")
Debug Mode — Debug mode should always be off in production:
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
Allowed Hosts — This controls which domains can serve your app:
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
Database — Replace the DATABASES setting with:
DATABASES = {
"default": dj_database_url.config(
default="sqlite:///db.sqlite3",
conn_max_age=600,
)
}
This uses the DATABASE_URL environment variable if it’s set (which it will be on Railway), and falls back to SQLite for local development.
WhiteNoise Middleware — Add WhiteNoise to the MIDDLEWARE list, right after SecurityMiddleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # add this line
"django.contrib.sessions.middleware.SessionMiddleware",
# ... rest of middleware
]
Static Files — Add these settings at the bottom of the file:
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
This tells Django where to collect static files and configures WhiteNoise to compress and cache them.
Creating the Procfile
Railway (and many other platforms) uses a Procfile to know how to run your app. Create a file called Procfile in the root of your project (the same directory as manage.py):
web: gunicorn bookmarks_project.wsgi
This tells the platform to run Gunicorn, pointing it to our project’s WSGI configuration.
Deploying to Railway
Railway is a modern hosting platform that makes deploying Django apps straightforward. Here’s how to get your app live.
Step 1: Push to GitHub
If you haven’t already, initialize a Git repository and push your code to GitHub:
git init
git add .
git commit -m "Initial commit - bookmark manager"
git branch -M main
git remote add origin https://github.com/yourusername/bookmarks-project.git
git push -u origin main
Make sure you have a .gitignore file that excludes things like myenv/, db.sqlite3, __pycache__/, and .env.
Step 2: Create a Railway Project
- Go to railway.app and sign in with your GitHub account.
- Click “New Project” and select “Deploy from GitHub repo.”
- Select your bookmarks project repository.
Step 3: Add a PostgreSQL Database
- In your Railway project dashboard, click “New” and select “Database” → “PostgreSQL.”
- Railway will automatically create a PostgreSQL instance and set the
DATABASE_URLenvironment variable for your app.
Step 4: Set Environment Variables
In your Railway project settings, add the following environment variables:
SECRET_KEY— Generate a secure random string. You can use Python:python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"DEBUG— Set toFalseALLOWED_HOSTS— Set to your Railway domain (something likeyour-app.up.railway.app)
Step 5: Run Migrations
In the Railway dashboard, open the “Settings” tab for your service and add a deploy command, or use the Railway CLI:
railway run python manage.py migrate
railway run python manage.py createsuperuser
This creates the database tables on your production PostgreSQL database and sets up an admin account.
Step 6: Go Live!
Railway automatically deploys your app whenever you push to your main branch. Once the deployment finishes, click the provided URL to see your bookmark manager running in production. You now have a live Django web application!
What We Built
Let’s look back at everything we accomplished across all five posts:
- Getting Started — We installed Python, set up a virtual environment, and installed Django.
- First Project — We created a Django project and app, wrote our first view, and set up templates.
- Models & Admin — We defined our Bookmark model, ran migrations, and explored the admin panel.
- Views & Templates — We built a full CRUD interface with listing, detail, create, and delete pages.
- Auth & Deployment — We added user authentication and deployed to Railway.
From zero to a deployed, authenticated web application — not bad for a bootcamp!
Where to Go from Here
Our bookmark manager is a solid foundation, but there’s so much more you could build on top of it. Here are some ideas to keep learning:
- Edit bookmarks — Add an update view so users can modify existing bookmarks without deleting and recreating them.
- Tag filtering — Let users click on a tag to see all bookmarks with that tag.
- User-specific bookmarks — Add a
ForeignKeyto theUsermodel so each user has their own private collection. - CSV import — Build a management command or upload form that lets users import bookmarks from a CSV file.
- REST API with Django REST Framework — Turn your bookmark manager into an API that mobile apps or browser extensions can use. Django REST Framework makes this surprisingly easy.
The best way to learn is to keep building. Pick one of these ideas and run with it — you have all the foundational knowledge you need.
If you have any questions or want to share what you’ve built, feel free to reach out to me on Mastodon at @joshfinnie@fosstodon.org. I’d love to hear from you!
Thanks for reading and happy coding! 🚀