Chapter 20: Styling and Deployment
Style with Bootstrap. Deploy to production.
Installing django-bootstrap5
pip install django-bootstrap5
# inv_project/settings.py
INSTALLED_APPS = [
# My apps
'inventory',
'accounts',
# Third party
'django_bootstrap5', (1)
# Default Django
# ...
]
| 1 | Note underscore in package name |
Updating Base Template
<!-- inventory/templates/inventory/base.html -->
{% load django_bootstrap5 %} (1)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server Inventory</title>
{% bootstrap_css %} (2)
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'inventory:index' %}">
Server Inventory
</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{% url 'inventory:servers' %}">
Servers
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto"> (3)
{% if user.is_authenticated %}
<li class="nav-item">
<span class="navbar-text me-2">
{{ user.username }}
</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:logout' %}">
Log out
</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:register' %}">
Register
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:login' %}">
Log in
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container">
{% block content %}{% endblock content %}
</main>
{% bootstrap_javascript %} (4)
</body>
</html>
Notes:
1. Load Bootstrap template tags
2. Include Bootstrap CSS
3. ms-auto pushes items to right
4. Include Bootstrap JavaScript
Styling Forms
<!-- registration/login.html -->
{% extends 'inventory/base.html' %}
{% load django_bootstrap5 %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<h2>Log in to Server Inventory</h2>
{% if form.errors %}
<div class="alert alert-danger">
Your username and password didn't match.
</div>
{% endif %}
<form action="{% url 'accounts:login' %}" method="post">
{% csrf_token %}
{% bootstrap_form form %} (1)
{% bootstrap_button button_type="submit" content="Log in" %}
</form>
</div>
</div>
{% endblock content %}
-
bootstrap_formrenders styled form
Styling Pages
Index Page
{% extends 'inventory/base.html' %}
{% block content %}
<div class="px-3 py-4 my-4 text-center bg-light rounded-3">
<h1 class="display-4">Infrastructure Dashboard</h1>
<p class="lead">
Track your servers, log events, monitor your infrastructure.
Register servers, record deployments, restarts, and alerts.
</p>
<a class="btn btn-primary btn-lg" href="{% url 'accounts:register' %}">
Register »
</a>
</div>
{% endblock content %}
Servers Page
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Servers</h1>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Hostname</th>
<th>IP Address</th>
<th>OS</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr>
<td>
<a href="{% url 'inventory:server_detail' server.id %}">
{{ server.hostname }}
</a>
</td>
<td><code>{{ server.ip_address }}</code></td>
<td>{{ server.os }}</td>
<td>
<span class="badge {% if server.status == 'active' %}bg-success{% elif server.status == 'maintenance' %}bg-warning{% else %}bg-secondary{% endif %}">
{{ server.status }}
</span>
</td>
</tr>
{% empty %}
<tr><td colspan="4">No servers registered.</td></tr>
{% endfor %}
</tbody>
</table>
<a href="{% url 'inventory:new_server' %}" class="btn btn-primary">
Register Server
</a>
{% endblock content %}
Server Detail Page
{% extends 'inventory/base.html' %}
{% block content %}
<h1>{{ server.hostname }}</h1>
<p class="text-muted">
<code>{{ server.ip_address }}</code> |
{{ server.os }} |
<span class="badge {% if server.status == 'active' %}bg-success{% else %}bg-warning{% endif %}">
{{ server.status }}
</span>
</p>
<p>
<a href="{% url 'inventory:log_event' server.id %}" class="btn btn-sm btn-primary">
Log Event
</a>
<a href="{% url 'inventory:edit_server' server.id %}" class="btn btn-sm btn-outline-secondary">
Edit
</a>
</p>
<h2>Events</h2>
{% for event in events %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between">
<span>
<span class="badge bg-info">{{ event.event_type }}</span>
{{ event.timestamp|date:'Y-m-d H:i' }}
</span>
</div>
<div class="card-body">
{{ event.message|linebreaks }}
</div>
</div>
{% empty %}
<p class="text-muted">No events logged.</p>
{% endfor %}
{% endblock content %}
Deployment Checklist
Before deploying:
-
Version control with Git
-
Requirements file
-
Production settings
-
Static files collection
-
Database configuration
Git Setup
# In project root
git init
Create .gitignore:
.venv/ __pycache__/ *.pyc db.sqlite3 .env *.log staticfiles/
git add .
git commit -m "Initial commit"
Requirements File
pip freeze > requirements.txt
Contents:
asgiref==3.7.2 Django==5.0 django-bootstrap5==23.3 # ...
Production Settings
Environment Variables
pip install python-dotenv
Create .env (never commit this):
DEBUG=False SECRET_KEY=your-actual-secret-key ALLOWED_HOSTS=inventory.example.com,www.inventory.example.com
Settings Configuration
# inv_project/settings.py
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-prod')
DEBUG = os.getenv('DEBUG', 'True') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
# Production: use whitenoise for static files
if not DEBUG:
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Install Production Dependencies
pip install gunicorn whitenoise psycopg2-binary
pip freeze > requirements.txt
Custom Error Pages
<!-- templates/404.html -->
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Page Not Found</h1>
<p>The page you requested ({{ request.path }}) does not exist.</p>
<p><a href="{% url 'inventory:index' %}">Return to dashboard</a></p>
{% endblock content %}
<!-- templates/500.html -->
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Server Error</h1>
<p>An error occurred. We're working on it.</p>
{% endblock content %}
Update settings:
TEMPLATES = [
{
# ...
'DIRS': [BASE_DIR / 'templates'], (1)
# ...
}
]
| 1 | Project-level templates directory |
Deployment Options
Platform.sh / Railway / Render
Modern PaaS options. General workflow:
-
Connect Git repository
-
Configure build settings
-
Set environment variables
-
Deploy
Procfile (for Heroku-style platforms)
web: gunicorn inv_project.wsgi
Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "inv_project.wsgi"]
Production Database
SQLite is for development. Use PostgreSQL in production:
# settings.py
import dj_database_url
if os.getenv('DATABASE_URL'):
DATABASES = {
'default': dj_database_url.config(conn_max_age=600)
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
Security Checklist
# Production settings
DEBUG = False
SECRET_KEY = os.getenv('SECRET_KEY') # From environment
# HTTPS settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Quick Reference
| Bootstrap Class | Purpose |
|---|---|
|
Centered content wrapper |
|
Styled table with alternating rows |
|
Navigation bar |
|
Styled button |
|
Content container |
|
Status indicator |
|
Error message |
|
Margin bottom |
|
Margin start auto (push right) |
| Deployment | Command/File |
|---|---|
Requirements |
|
Static files |
|
Gunicorn |
|
Environment |
|
Database |
|
Exercises
20-1. Server Status Colors
Add CSS classes for different server statuses (active=green, maintenance=yellow, decommissioned=red).
20-2. Event Filtering
Add dropdown to filter events by type (deploy, restart, alert, maintenance).
20-3. Export to CSV
Add button to export server list to CSV format.
20-4. Live Deployment
Deploy your server inventory to a cloud platform.
Summary
-
django-bootstrap5 integrates Bootstrap with Django
-
{% bootstrap_form %}renders styled forms -
Bootstrap classes style tables, badges, buttons
-
Production requires: DEBUG=False, secret key, HTTPS
-
Use environment variables for sensitive settings
-
Static files: collectstatic + whitenoise
-
PostgreSQL for production databases
-
Docker enables consistent deployment
-
Custom 404/500 pages improve user experience
Course Complete
You’ve built:
-
Alien Invasion - Game with pygame, OOP, sprites, collision detection
-
Data Visualization - matplotlib, Plotly, CSV/JSON parsing, APIs
-
Server Inventory - Django web app with users, forms, deployment
Core skills acquired: - Python fundamentals: data types, control flow, functions, classes - File handling and data processing - Testing with pytest - Web development with Django - API integration - Deployment workflows
Continue learning: - Django REST Framework for APIs - Celery for background tasks - Redis for caching - Docker and Kubernetes for orchestration - CI/CD pipelines
The best way to learn is to build. Start your own project.