Web App Development with Docker Compose
Docker Compose is a powerful tool that simplifies managing multi-container Docker applications, making it especially useful for web app development. With Docker Compose, you can easily define and manage services like databases, web servers, and other dependencies in a single configuration.
In this guide, we’ll walk you through how to leverage Docker Compose and Nginx to efficiently set up a local development and production-ready environment for a Django application with PostgreSQL as the database. This guide will provide a real-world example of how Docker Compose, Gunicorn, and Nginx can work together to streamline web application deployment.
While we focus on setting up the environment with Docker, we won’t go into deep details on Django’s internal structure. However, this guide will help you understand how to efficiently containerize and deploy a Django app using Nginx as a reverse proxy.
The project files for this section are available in our GitHub repository. If you'd prefer to skip the detailed steps of Django web app development, simply clone the project files and proceed to Step 12.
If you'd like to learn more about Django, check out our Django Introduction Guide.
Step 1: Set Up Project Environment
Before configuring Docker and Docker Compose, we need to set up the project directory structure to support Django, PostgreSQL, and Nginx.
By running the commands below, you'll have the following project structure in place, ready for Docker Compose to orchestrate the services:
mkdir ch6-docker-compose-demo && cd ch6-docker-compose-demo
mkdir -p django postgres/data nginx
touch docker-compose.yml django/Dockerfile django/requirements.txt nginx/nginx.conf
Project Structure:
ch6-docker-compose-demo/
├── django/
│ ├── Dockerfile
│ ├── requirements.txt
├── nginx/
│ ├── nginx.conf
├── postgres/
│ ├── data/
├── docker-compose.yml
Step 2: Write the Dockerfile for Django
To containerize our Django application, we'll create a Dockerfile. This file defines the steps to build a Docker image for our Django app, ensuring that it has all the necessary dependencies and is set up to run smoothly inside a container.
In this step, we'll walk you through writing the Dockerfile
for Django. This file will specify the base image, install dependencies, copy project files, and configure the app to run inside the container. Using Docker Compose, we can then manage this container alongside other services like the database.
Add the following content to the django/
Dockerfile
:
FROM python:3.9-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
libpq-dev gcc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]
Explanation of the Dockerfile:
- FROM python:3.9-slim: Python 3.9 slim image is a lightweight version of Python that contains the essentials needed to run a Python app, reducing the overall image size.
- WORKDIR /app: This sets
/app
as the working directory inside the container. All subsequent commands will run in this directory. - RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/: Install necessary system packages like
libpq-dev
(needed for PostgreSQL database interaction) andgcc
(a C compiler required for some Python dependencies). After the installation, we clean up the package list to minimize the image size. - COPY requirements.txt .: copies the
requirements.txt
file from your local project into the container. This file contains the list of Python dependencies required by the project. - RUN pip install --no-cache-dir -r requirements.txt: This command installs the dependencies listed in
requirements.txt
using pip. The--no-cache-dir
option ensures that pip doesn’t store cached files, further reducing the image size. - COPY . .: Copies all the files from the current directory (i.e., your Django app) into the container's
/app
directory. - RUN python manage.py collectstatic --noinput: This command collects static files before running the app
- CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]: Finally, we specify the command to start the application using Gunicorn, a production-grade web server for Python applications. This command binds the app to
0.0.0.0:8000
(making it accessible externally on port 8000) and tells it to use the WSGI entry point inconfig.wsgi:application
to run the app.
Step 3: Write the docker-compose.yml File
In this step, we'll define the Docker Compose configuration that will set up the necessary containers for our Django application, PostgreSQL database, and Nginx server. Docker Compose allows us to define multi-container applications in a single YAML file, which simplifies the process of starting and managing our environment with just one command. This configuration will not only build the web app container but also link it to a database container and an Nginx container, allowing the three services to communicate seamlessly.
services:
web:
build: ./django
container_name: django_app
expose:
- "8000"
depends_on:
- db
environment:
- DEBUG=True
- DB_NAME=postgres
- DB_USER=postgres
- DB_PASSWORD=postgres
- DB_HOST=db
- DB_PORT=5432
volumes:
- ./django:/app
- ./staticfiles:/app/staticfiles
db:
image: postgres:13
container_name: postgres_db
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
nginx:
image: nginx:latest
container_name: nginx
restart: always
ports:
- "80:80"
depends_on:
- web
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro # Load custom Nginx config
- ./staticfiles:/app/staticfiles # Serve static files
volumes:
postgres-data:
Explanation of docker-compose.yml
File:
services:
This section defines the three main services needed for deployment.
web
(Django application using Gunicorn)
This service runs the Django app inside Gunicorn.
build: ./django
→ Tells Docker Compose to build the image from the./django
directory, where theDockerfile
is located.container_name: django_app
→ Names the containerdjango_app
for easier management.expose: - "8000"
→ Makes port 8000 accessible internally to Nginx (but does not expose it to the host machine).depends_on: db
→ Ensures the PostgreSQL container (db
) starts beforeweb
.environment
→- Defines database connection variables so Django can connect to PostgreSQL.
volumes
→./django:/app
→ Mounts the local Django project directory into the container for development../staticfiles:/app/staticfiles
→ Ensures static files are accessible to Nginx.
db
(PostgreSQL database)
This service runs a PostgreSQL database to store application data.
image: postgres:13
→ Uses the official PostgreSQL image (version 13).container_name: postgres_db
→ Names the database container aspostgres_db
.restart: always
→ Ensures the database restarts automatically if stopped.environment
→- Defines database credentials (username, password, and database name).
volumes
→postgres-data:/var/lib/postgresql/data
→ Stores PostgreSQL data outside the container so it persists even if the container is removed.
ports: - "5432:5432"
→ Exposes PostgreSQL port 5432 to allow external database connections (useful for local development).
nginx
(Reverse proxy for handling requests and serving static files)
This service forwards client requests to Django and serves static files.
image: nginx:latest
→ Uses the latest official Nginx image.container_name: nginx
→ Names the containernginx
.restart: always
→ Ensures Nginx restarts automatically if stopped.ports: - "80:80"
→ Maps port 80 of the container to port 80 on the host machine (making the app accessible viahttp://localhost
).depends_on: web
→ Ensures the Django app (web
) starts before Nginx.volumes
→./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
→ Loads a custom Nginx configuration file to properly route requests../staticfiles:/app/staticfiles
→ Serves static files (CSS, JavaScript) directly.
volumes:
This section defines the persistent data storage for the PostgreSQL database. The postgres-data
volume ensures that data persists even if the database container is recreated.
This docker-compose.yml
file provides a complete environment for running a Django app with PostgreSQL and Nginx, with automatic handling of the dependencies, network, and data persistence.
Step 4: Configure Nginx
In this step, we configure Nginx as a reverse proxy to handle client requests and serve static files efficiently. Instead of exposing Django’s built-in server directly, Nginx forwards requests to the Django app running in the web
container while also serving static files directly.
server {
listen 80;
server_name localhost;
location /static/ {
alias /app/staticfiles/;
expires 1y;
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Nginx is set up to:
- Listen on Port 80
Nginx listens for incoming HTTP requests on port 80. Theserver_name
is set tolocalhost
for local development but should be updated to a domain name in production. - Serve Static Files Efficiently
Static files are served directly by Nginx from thestaticfiles
directory inside the container. This improves performance by reducing the load on Django. Browser caching is enabled to minimize repeated requests for unchanged static files. - Forward Requests to Django
All non-static requests are forwarded to the Django app running inside theweb
container`. This allows Django to handle application logic while Nginx takes care of routing and serving assets.
Step 5: Install Dependencies
Before we can run our Django application, we need to specify the necessary Python packages that the project will rely on. In this step, we will create a requirements.txt
file, which is the standard way of listing Python dependencies for a project. This file allows us to easily install the required libraries using pip
, ensuring that our development environment is consistent and reproducible across different machines or containers.
To get started, let's create the requirements.txt
file inside the django
directory. This file will include the essential packages needed to run the Django app with PostgreSQL and to serve the app in a production-like environment.
Create django/requirements.txt
and add the following content:
Django==4.2
gunicorn
psycopg2-binary
Explanation of the requirements.txt file:
- Django==4.2: This specifies that we are using version
4.2
of Django, which is the web framework for our app. This is a stable version and includes all the latest features and security updates. - gunicorn: Gunicorn is a Python WSGI HTTP server for running Django in a production environment. While Django’s built-in development server is sufficient for testing, Gunicorn is commonly used to handle production-level traffic.
- psycopg2-binary: This is a PostgreSQL adapter for Python. It allows Django to interact with PostgreSQL databases by connecting and executing queries. We use the binary version because it includes the necessary dependencies for PostgreSQL, simplifying installation.
With this requirements.txt
file in place, we’ll be able to install all the necessary dependencies in one go, ensuring that our app is ready for development.
Step 6: Create Django App & Initial Project Files
In this step, we will set up the foundation of our Django application by creating the initial project and app files. First, we will use Docker Compose to run Django commands inside the container to create the main project structure. We’ll also generate an app for managing our book-related functionality. This process ensures that all necessary files and configurations are set up correctly in the Docker environment.
docker-compose run --rm web django-admin startproject config /app
docker-compose run --rm web python manage.py startapp library
Once the project and app are created, we will manually add a couple of essential files, such as urls.py
and forms.py
, to get started with the app's routing and form handling. Let’s walk through these steps to get our Django application ready for development.
Create missing project files:
touch django/library/urls.py django/library/forms.py
Step 7: Configure Django Settings
In this step, we will partially update the Django settings file (config/settings.py
) to integrate essential configurations for our project. The updates will include setting up static file handling, installing the library
app, configuring templates, and defining database connections using environment variables.
settings.py
Modify the existing config/settings.py
file by adding or updating the following sections:
# Add the os module
import os
# Define the base directory for the project
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'False') == 'True'
# Defines which domains or IPs can access the Django application.
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",")
# List of installed Django apps, including 'library' (our custom app)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'library', # Our custom app
]
# Template configuration
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'library/templates')], # Template directory for our app
'APP_DIRS': True, # Allows searching for templates in app directories
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# Database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'postgres'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', 'postgres'),
'HOST': os.getenv('DB_HOST', 'db'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
# settings.py (for development)
STATIC_URL = '/static/'
# Define STATIC_ROOT to avoid errors with collectstatic
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
Key Changes and Explanations:
- Import os: The os module in Python is used to interact with the operating system, allowing Django to work with file paths, environment variables, and system configurations dynamically.
- In settings.py, the os module is mainly used for:
- Defining file paths dynamically using BASE_DIR
- Base Directory Setup:
BASE_DIR
is defined to help set paths relative to the root of the project. This is important for defining static file paths and templates dynamically. - Debug Mode:
DEBUG
is controlled using an environment variable (DEBUG=True
orDEBUG=False
).- When
DEBUG=True
, Django serves static files during development. - In production (
DEBUG=False
), Django only usesSTATIC_ROOT
, and Nginx serves static files.
- Allowed Hosts (
ALLOWED_HOSTS
)- Defines which domains or IPs can access the Django application.
- The value is retrieved from an environment variable (
ALLOWED_HOSTS
). - Defaults to
"localhost"
if not set.
- Installed Apps: We've included
'library'
inINSTALLED_APPS
, which is our custom app. This ensures that Django knows to look for this app when setting up models, views, and templates. - Template Configuration (
TEMPLATES
):DIRS
: This points to the templates directory inside ourlibrary
app (library/templates
). It tells Django where to look for HTML files.APP_DIRS
: This is set toTrue
so Django will also look inside app directories for templates.
- Database Configuration (
DATABASES
):- We’re using PostgreSQL as the database backend (
'ENGINE': 'django.db.backends.postgresql'
). - The database credentials (name, user, password, host, and port) are configured via environment variables (using
os.getenv
). This makes it easier to switch between different environments (e.g., local vs production). If environment variables are not set, default values like'postgres'
are used.
- We’re using PostgreSQL as the database backend (
- Static Files Configuration (
STATIC_URL
andSTATIC_ROOT
):STATIC_URL
tells Django where to look for static files (e.g., CSS, JavaScript).STATIC_ROOT=os.path.join(BASE_DIR, 'staticfiles')
specifies where static files are stored when runningcollectstatic
.
.env
To securely store sensitive settings and environment-specific configurations, create a .env
file in the project’s root directory. This file will contain key environment variables used by Django.
SECRET_KEY=your-very-secure-secret-key
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
- SECRET_KEY: Required for Django’s security-related features. This should be kept secret and never hardcoded in
settings.py
. - DEBUG: Set to
True
for development. In production, change this toFalse
to disable debug mode. - ALLOWED_HOSTS: Defines which hosts can access the Django application. Multiple values should be comma-separated.
docker-compose.yml
To ensure Django can access the environment variables from .env
, update docker-compose.yml
to reference the .env
file. This allows Docker to pass the variables to the web
container when it starts.
services:
web:
env_file:
- .env
environment:
- DB_NAME=postgres
- DB_USER=postgres
- DB_PASSWORD=postgres
- DB_HOST=db
- DB_PORT=5432
env_file: - .env
: Loads all variables from the.env
file into the container.environment:
: Defines additional environment variables for PostgreSQL that are not stored in.env
.
Note: In this guide, database settings are defined in docker-compose-prod.yml
for simplicity. However, in a real-world scenario, it is best to store all environment variables in the .env
file for better security and maintainability.
Step 8: Define Models and Register in Django Admin
1. Define Models in library/models.py
In this step, we will define two models: Category
and Book
. The Category
model will store categories like "Fiction" or "Nonfiction". The Book
model will represent individual books and their associated categories.
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
def __str__(self):
return self.title
Explanation:
- Category Model: This model has a single field, name, which will store the category name.
- Book Model: This model has a title field for the book's name and a category field, which is a foreign key to the Category model. This sets up a relationship between books and categories.
2. Run Migrations
Django uses migrations to apply changes to the database schema. After defining the models, you need to generate and apply migrations to update the database.
docker-compose run --rm web python manage.py makemigrations library
docker-compose run --rm web python manage.py migrate
Explanation:
makemigrations
: This command creates migration files based on the changes you've made to the models (like adding or modifying fields).migrate
: This applies the migrations, updating the database schema to match the defined models.
Run --rm
The commands above run a one-time container for the web
service defined in a docker-compose.yml
file and execute the python manage.py makemigrations library
or python manage.py migrate
command inside it.
Breaking Down the Command:
docker-compose run
: Starts a new container for a service defined indocker-compose.yml
without affecting running containers.--rm
: Automatically removes the container once it exits, ensuring it does not persist after execution.web
: The name of the service (typically defined indocker-compose.yml
).python manage.py migrate
: The actual command executed inside the container, commonly used in Django applications to apply database migrations.
What is the --rm Option?
The --rm
flag ensures that the temporary container created by docker-compose run
is automatically deleted after it completes execution. This prevents unnecessary accumulation of stopped containers.
Key Benefits of --rm:
- Keeps the system clean by preventing leftover stopped containers.
- Reduces manual cleanup since the container is automatically removed.
- Useful for one-time commands where you don’t need to keep the container after execution.
Differences Between docker-compose run and docker exec
Feature |
docker-compose run |
docker exec |
Purpose |
Creates and runs a new container for a service. |
Runs a command inside an existing container. |
Container Lifecycle |
Creates a temporary container (deleted with |
Uses an already running container. |
Use Case |
Running one-time tasks that do not require an existing container (e.g., migrations, debugging). |
Running commands in a container that is already running (e.g., shell access, monitoring). |
Example |
|
|
When to Use Which?
- Use
docker-compose run
when you need to create a separate temporary container to execute a command. - Use
docker exec
when you want to run a command inside an already running container.
For database migrations like in the given command, docker-compose run --rm
is preferred because it ensures that a new instance of the service runs with the latest environment configurations without interfering with the running application.
3. Register Models in Django Admin
To manage the Book
and Category
models via the Django admin interface, we need to register them in the admin.py
file.
from django.contrib import admin
from .models import Book, Category
admin.site.register(Book)
admin.site.register(Category)
Explanation:
admin.site.register()
: This registers theBook
andCategory
models with Django’s admin interface, allowing them to be managed (added, edited, deleted) through the web interface.
4. Populate Default Categories (Fiction & Nonfiction)
Now, we want to add default categories to the Category
model (e.g., "Fiction" and "Nonfiction") for use in the app.
docker-compose run --rm web python manage.py shell
Inside the shell:
from library.models import Category
Category.objects.get_or_create(name="Fiction")
Category.objects.get_or_create(name="Nonfiction")
print("Default categories created!")
exit()
Explanation:
get_or_create()
: This method ensures that the categories "Fiction" and "Nonfiction" are added to the database if they don’t already exist.- Django Shell: The
python manage.py shell
command opens a Python shell with Django's environment loaded, allowing you to interact with your models.
Step 9: Implement CRUD & Configure URLs
In this step, we will implement the CRUD functionality for managing books. This includes creating forms for adding or updating books, defining views to handle these actions, and configuring URLs to link the views to specific routes.
1. Edit library/forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'category']
Explanation:
- We create a form (
BookForm
) based on theBook
model. - The form will only include the
title
andcategory
fields, as defined in thefields
attribute.
2. Edit library/views.py
from django.shortcuts import render, redirect, get_object_or_404
from .models import Book, Category
from .forms import BookForm
def book_list(request):
books = Book.objects.all()
return render(request, 'index.html', {'books': books})
def book_create(request):
form = BookForm(request.POST or None)
if form.is_valid():
form.save()
return redirect('book_list')
return render(request, 'book_form.html', {'form': form})
def book_update(request, pk):
book = get_object_or_404(Book, pk=pk)
form = BookForm(request.POST or None, instance=book)
if form.is_valid():
form.save()
return redirect('book_list')
return render(request, 'book_form.html', {'form': form})
def book_delete(request, pk):
book = get_object_or_404(Book, pk=pk)
if request.method == 'POST':
book.delete()
return redirect('book_list')
return render(request, 'book_confirm_delete.html', {'book': book})
Explanation:
book_list
: Retrieves and displays all books from the database.book_create
: Handles creating a new book by saving a submitted form.book_update
: Allows updating an existing book by binding the form to a specific book instance.book_delete
: Manages the deletion of a book, requiring a confirmation via POST.
3. Edit library/urls.py
from django.urls import path
from .views import book_list, book_create, book_update, book_delete
urlpatterns = [
path('', book_list, name='book_list'),
path('new/', book_create, name='book_create'),
path('edit/<int:pk>/', book_update, name='book_update'),
path('delete/<int:pk>/', book_delete, name='book_delete'),
]
Explanation:
book_list
: The URL for viewing the list of books.book_create
: The URL for creating a new book.book_update
: The URL for editing a book, which includes the book's primary key in the URL.book_delete
: The URL for deleting a book, also using the primary key.
4. Update config/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('library.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
Explanation:
admin/
: URL for Django's built-in admin panel.''
(empty path): The root URL includes thelibrary
app's URLs, so any request to the base URL will be handled by the app.- The
static()
function allows Django to serve static files (like CSS, JavaScript) during development whenDEBUG
isTrue
.
Key Points of This Step:
- Forms (
forms.py
): We created aBookForm
based on theBook
model to handle the input of the book’s title and category. - Views (
views.py
): We implemented the core CRUD functionality—list, create, update, and delete—which is the backbone of our app’s user interactions. - URLs (
urls.py
): We set up URL routes for each CRUD action so users can access specific pages (list, create, update, delete) via HTTP requests. - Main URL Config (
config/urls.py
): The project’s main URL configuration includes paths to both the Django admin and the app's URLs, and handles static files during development.
This setup allows you to manage book entries in your app, with the ability to create, update, view, and delete them. The next steps involve testing these views and ensuring that everything works as expected. Let me know if you need more details!
Step 10: Create HTML Templates & CSS
In this step, we’ll create the HTML templates and CSS styles. These templates will define the structure of the pages, such as the book list, add/edit forms, and delete confirmation.
1. Create Template and Static Directories
mkdir -p django/library/templates
touch django/library/templates/index.html django/library/templates/book_form.html django/library/templates/book_confirm_delete.html
mkdir -p django/library/static/css
touch django/library/static/css/style.css
2. Create index.html Template
The index.html
template displays the list of books.
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Book List</title>
<link
rel="stylesheet"
type="text/css"
href="{% static 'css/style.css' %}"
/>
</head>
<body>
<div class="container">
<h1>Book List</h1>
<a href="{% url 'book_create' %}" class="btn-add">Add New Book</a>
<ul class="book-list">
{% for book in books %}
<li class="book-item">
<span class="book-info"
>{{ book.title }} ({{ book.category.name }})</span
>
<div class="button-group">
<a href="{% url 'book_update' book.pk %}" class="btn edit">Edit</a>
<a href="{% url 'book_delete' book.pk %}" class="btn delete"
>Delete</a
>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>
3. Create book_form.html Template
The book_form.html
template is used for both adding a new book and editing an existing one.
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
{% if form.instance.pk %}Edit Book{% else %}Add New Book{% endif %}
</title>
<link
rel="stylesheet"
type="text/css"
href="{% static 'css/style.css' %}"
/>
</head>
<body>
<div class="container">
<h1>
{% if form.instance.pk %}Edit Book{% else %}Add New Book{% endif %}
</h1>
<form method="post">
{% csrf_token %}
<div class="form-group">
{{ form.title.label_tag }} {{ form.title }}
</div>
<div class="form-group">
{{ form.category.label_tag }} {{ form.category }}
</div>
<div class="button-container">
<button type="submit" class="btn">Save</button>
<a href="{% url 'book_list' %}" class="btn">Back to Book List</a>
</div>
</form>
</div>
</body>
</html>
4. Create book_confirm_delete.html Template
The book_confirm_delete.html
template is used for confirming the deletion of a book.
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Delete Book</title>
<link
rel="stylesheet"
type="text/css"
href="{% static 'css/style.css' %}"
/>
</head>
<body>
<div class="container delete-container">
<h1>Delete Book</h1>
<p>Are you sure you want to delete "{{ book.title }}"?</p>
<div class="button-container">
<form method="post">
{% csrf_token %}
<button type="submit" class="btn">Yes, Delete</button>
<a href="{% url 'book_list' %}" class="btn btn-cancel">Cancel</a>
</form>
</div>
</div>
</body>
</html>
5. Create style.css
The style.css
file provides initial styling for the app. While it's not complete at this stage, we'll show you how to update and enhance it later in the tutorial.
body {
font-family: "Arial", sans-serif;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
color: #333;
line-height: 1.6;
display: flex;
justify-content: center;
}
.container {
max-width: 800px;
width: 100%;
background: #f0f0f0;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
font-size: 24px;
margin-bottom: 20px;
color: #222;
}
Step 11: Collect Static Files & Create Superuser
In this step, we will prepare the Django app to handle static files and set up a superuser account to manage the app through the Django admin panel. Static files, like CSS and JavaScript, need to be collected into a directory where they can be served by the web server. We'll also create a superuser account that gives you access to the Django admin interface, allowing you to manage your app's data, including the books, categories, and users.
1. Collect Static Files
The collectstatic
command gathers all static files (CSS, JavaScript, images, etc.) into the directory specified by STATIC_ROOT
in your settings.py
. This step is necessary for production environments or when you're using a web server like Nginx to serve static files. The --noinput
flag prevents Django from asking for confirmation during the process.
docker-compose run --rm web python manage.py collectstatic --noinput
2. Create a Superuser
The createsuperuser
command creates an administrative user who can log into the Django admin panel. This user will have full access to manage your app’s content, including CRUD operations on books, categories, and any other models you create.
docker-compose run --rm web python manage.py createsuperuser
You’ll be prompted to enter details like username, email, and password for the superuser account.
Step 12: Run and Test the App
Now that we've set up the project, it's time to run the app and make sure everything is working as expected. In this step, we’ll restart the containers to ensure all services are up and running, test the app's user interface, and access the Django admin panel to manage the app.
Restart the Containers
To ensure that the latest changes are applied and the containers are running with the most up-to-date configurations, we'll restart them using the following command. This command will start or restart the containers in detached mode, which means they will run in the background without occupying your terminal.
It's essential to restart the containers after creating a superuser, as this ensures the app is fully functional and ready for use.
docker-compose up -d
Explanation:
docker-compose up -d
starts all the services defined in yourdocker-compose.yml
file and runs them in the background.- This ensures that the app, database, and any other services are correctly initialized with the latest settings.
Running the App Using GitHub Project Files
If you skipped Steps 1-11 and jumped directly to this step, follow the steps below.
1. Clone the Repository
If you haven't cloned the project yet, do so using:
git clone https://github.com/bloovee/ch6-docker-compose-demo.git
cd ch6-docker-compose-demo
2. Build and Start the Containers
Run the following command to build the Docker image and start the containers:
docker-compose up --build -d
This does the following:
--build
forces a rebuild of the Docker image.-d
runs the containers in detached mode (in the background).
4. Apply Database Migrations
Run the migrations to create the necessary database tables:
docker-compose run --rm web python manage.py migrate
5. Collect Static Files
Ensure static files are properly collected:
docker-compose run --rm web python manage.py collectstatic --noinput
This gathers all static files into the STATIC_ROOT
directory.
6. Create a Superuser
To access the Django admin panel, create a superuser:
docker-compose run --rm web python manage.py createsuperuser
Follow the prompts to set up an admin username, email, and password.
Check Django Admin
To manage your app via the Django admin interface, open the following URL:
The Django admin panel allows you to manage all the data within your app, such as adding, editing, or deleting books. You’ll need to log in with the superuser credentials created earlier to access this panel.
If you skipped Steps 1-10 and jumped directly to this step, you’ll need to add some category data before testing the app UI in the next step.
Test the App UI
Once the containers are up and running, you can test the user interface (UI) of the app by opening the following URL in your browser:
This URL will load the book list page, where you can see the books, add new ones, and perform CRUD operations. If everything is set up correctly, the app’s frontend should be fully functional.
Step 13: Modify the App
In this step, we’ll walk you through the process of updating and refining the app's user interface. Specifically, we’ll focus on modifying the CSS to enhance the styling of the app and complete the UI design.
1. Edit the style.css File
To begin, you'll need to update the style.css
file, which is located in the templates
folder. You can either replace the existing CSS with the following code or make your own custom modifications.
body {
font-family: "Arial", sans-serif;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
color: #333;
line-height: 1.6;
display: flex;
justify-content: center;
}
.container {
max-width: 800px;
width: 100%;
background: #f0f0f0;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
font-size: 24px;
margin-bottom: 20px;
color: #222;
}
.btn {
display: inline-block;
padding: 10px 15px;
background: #777;
color: white;
border-radius: 5px;
text-decoration: none;
border: none;
font-size: 14px;
transition: background 0.3s ease-in-out;
width: 150px;
text-align: center;
}
.btn:hover {
background: #555;
}
.btn-add {
display: block;
width: fit-content;
margin: 10px auto;
padding: 10px 15px;
background: #4682b4;
color: white;
border-radius: 5px;
text-decoration: none;
border: none;
font-size: 14px;
transition: background 0.3s ease-in-out;
text-align: center;
}
.btn-add:hover {
background: #2f4f6f;
}
.btn-cancel {
margin-top: 10px;
width: 120px;
}
.btn.edit,
.btn.delete {
width: 70px;
}
.book-list {
list-style: none;
padding: 0;
}
.book-item {
display: flex;
justify-content: space-between;
align-items: center;
background: white;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 10px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
.book-info {
flex-grow: 1;
}
.button-group {
display: flex;
gap: 10px;
}
form {
background: transparent;
padding: 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
}
.form-group {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 15px;
}
input,
select {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.button-container {
display: flex;
justify-content: center;
gap: 15px;
width: 100%;
}
.delete-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.delete-container .button-container {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin: auto;
gap: 15px;
width: 100%;
margin-top: 20px;
}
2. Re-run the Container
After updating the CSS file, you need to ensure that the changes take effect by running the following commands:
docker-compose run --rm web python manage.py collectstatic --noinput
docker-compose restart
These commands will:
- Collect static files and apply the updated styles.
- Restart the container to load the new changes.
Once that’s done, head over to http://localhost/ and refresh your browser to see the updated UI.
By using Docker Compose, redeploying your multi-container app becomes incredibly straightforward. Whether you're updating your application’s code, configurations, or dependencies, Docker Compose allows you to rebuild and restart containers quickly with just a few commands. This ensures you can easily make changes, test them, and redeploy your app without hassle—saving you time and simplifying the management of a complex development environment.
Final Step: Clean Up
Now that you’ve completed the local development setup, it’s important to clean up your environment by stopping and removing the containers, volumes, and images created during the development process. This helps keep your system organized and prevents unnecessary resource consumption. To do so, run the following command:
docker-compose down --volumes --rmi all
This command will stop the containers, remove the associated volumes (which store database data and other persistent files), and delete the images built during development. It’s a good practice to clean up after each session, especially when you’re working in a containerized environment.
In the next section, we’ll shift focus to deploying the app in a production environment.
FAQ: Web App Development with Docker Compose
What is Docker Compose and why is it useful for web app development?
Docker Compose is a tool that simplifies managing multi-container Docker applications. It is particularly useful for web app development because it allows you to define and manage services like databases, web servers, and other dependencies in a single configuration file.
How does Docker Compose help in setting up a Django application?
Docker Compose helps set up a Django application by orchestrating multiple services such as the Django app itself, a PostgreSQL database, and an Nginx server. It allows you to define these services in a single YAML file, making it easy to start and manage the environment with one command.
What role does Nginx play in this setup?
Nginx acts as a reverse proxy in this setup. It handles client requests, serves static files efficiently, and forwards non-static requests to the Django application running inside the web container. This setup improves performance and security.
Why is it important to use environment variables in Django settings?
Using environment variables in Django settings is important for security and flexibility. It allows you to keep sensitive information like database credentials and secret keys out of the codebase, and makes it easier to switch between different environments, such as development and production.
What is the purpose of the `collectstatic` command in Django?
The `collectstatic` command in Django gathers all static files from your apps and collects them into a single directory specified by `STATIC_ROOT`. This is necessary for serving static files in production environments, especially when using a web server like Nginx.