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 %}
  1. bootstrap_form renders 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 &raquo;
    </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:

  1. Version control with Git

  2. Requirements file

  3. Production settings

  4. Static files collection

  5. 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:

  1. Connect Git repository

  2. Configure build settings

  3. Set environment variables

  4. 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

container

Centered content wrapper

table table-striped

Styled table with alternating rows

navbar

Navigation bar

btn btn-primary

Styled button

card

Content container

badge bg-success

Status indicator

alert alert-danger

Error message

mb-4

Margin bottom

ms-auto

Margin start auto (push right)

Deployment Command/File

Requirements

pip freeze > requirements.txt

Static files

python manage.py collectstatic

Gunicorn

gunicorn project.wsgi

Environment

.env file with python-dotenv

Database

dj_database_url package

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:

  1. Alien Invasion - Game with pygame, OOP, sprites, collision detection

  2. Data Visualization - matplotlib, Plotly, CSV/JSON parsing, APIs

  3. 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.