Create a Dockerfile. This Dockerfile uses a multi-stage build to create a smaller final image.
# Python 3.13-slim image is used as the base image for the builder stage
# Using a specific digest ensures that the exact same image is used every time,
# which can help with reproducibility and security.
FROM python@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder
# Create a working directory for the application
RUN mkdir /app
# Set the working directory to /app
WORKDIR /app
# Set environment variables to prevent Python from writing .pyc files
ENV PYTHONDONTWRITEBYTECODE=1
# Ensure that output is not buffered, which can be helpful for debugging and logging.
ENV PYTHONUNBUFFERED=1
# Copy the requirements.txt file into the container.
COPY requirements.txt /app
# Upgrade pip to the latest version to ensure that we have the latest features and security fixes.
RUN pip install --upgrade pip
# Install the dependencies listed in requirements.txt. The --no-cache-dir option is used to
# prevent pip from caching the downloaded packages, which can help reduce the size of the final image.
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# The second stage of the Docker will create the final image that will be used to run the Django application.
FROM python@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3
# Create a non-root user named 'app' and set up the application directory.
# This is a security best practice to avoid running the application as the root user.
RUN useradd -m -r appuser && mkdir /app && chown appuser:appuser /app
# Set the working directory to /app, which is where the Django application will be located.
WORKDIR /app
# Copy the installed dependencies from the builder stage to the final image.
# This includes the Python packages that were installed in the builder stage.
COPY --from=builder /install /usr/local
# Copy the application code into the container. The --chown=app:app option ensures that the files are
# owned by the 'app' user, which is important for security and proper file permissions.
COPY --chown=appuser:appuser . .
# Collect static files for the Django application.
# The --noinput option is used to prevent Django from prompting for any input during the collection process,
# which is important when running in a non-interactive environment like a Docker container.
RUN python manage.py collectstatic --noinput
# Set environment variables again in the final image to ensure that they are available when the application is running.
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Switch to the 'app' user to run the application with non-root privileges, which is a security best practice.
USER appuser
# Expose port 8000.
EXPOSE 8000
# Use Gunicorn to serve the Django application. The --bind option specifies that the application should
# listen on all available interfaces
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "5", "app.wsgi:application", "--timeout", "120"]
Create a docker-compose.yml file to define the services for the Django application.
services: # Declare the services in the Docker Compose file.
app: # app service
build: . # Use Dockerfile in the current directory to build the image for this service.
container_name: app # Container name.
ports:
- "8000:8000" # Host:Container. Port 8000 is exposed on the machine.
restart: always # This ensures that the container will automatically restart if it crashes or if the Docker daemon is restarted.
environment: # Environment variables
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} # The secret key for the Django application
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS} # The allowed hosts for the Django application, which is a security measure to prevent HTTP Host header attacks.
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
env_file:
- .env # Create a .env file in the project root directory or provide a path from host machine.
networks:
- app-network # This allows the services to communicate with each other using DNS.
depends_on: # Database should be up and running before the app.
db:
condition: service_healthy # App will start if DB is healthy.
db: # DB service for PostgreSQL database
image: postgres@sha256:ab47a724d0e37131ac950b98ff2beeb3fdb3bfcc592d73c6cc4b4e7626649671 # postgres:16 image from Dockerhub
restart: always # This ensures that the container will automatically restart if it crashes or if the Docker daemon is restarted.
environment: # Environment variables for the PostgreSQL database. These are used to configure the database and set up the initial database, user, and password.
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
networks:
- app-network # This specifies that the service should be connected to the 'app-network', allowing it to communicate with the 'app' service.
volumes:
- postgres_data:/var/lib/postgresql/data # Persists data on container restarts
healthcheck: # This defines a health check for the PostgreSQL service to ensure that it is running and ready to accept connections before the 'app' service starts.
test: [ "CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB" ]
interval: 1s
timeout: 5s
retries: 10
networks: # This section defines the custom network that the services will use to communicate with each other.
app-network:
driver: bridge
volumes: # This section defines the named volume for PostgreSQL data persistence.
postgres_data:
These Steps will help you to run your app locally.
Important:
Before deploying to production remove these lines from the app service in the docker-compose.yml file.
ports:
- "8000:8000" # Host:Container. Port 8000 is exposed on the machine.
Before deploying to production, make sure to set the environment variables in the .env file and also
configure a Revere Proxy. For container environments Traefik is recommended. Add the below configuration
to the existing docker-compose.yml file. I adopted the Traefik configuration from the Official Treafik documentation.
You can modify it as per your needs.
I have used https and Let's Encrypt for SSL certificate management. Make sure to replace the email address in the configuration with your own email address to receive notifications about certificate renewals and other important information from Let's Encrypt.
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- app-network
ports:
- "80:80"
- "443:443"
volumes:
- ./dynamic:/dynamic:ro # Dynamic configurations for Router and services
- ./certs/acme.json:/certs/acme.json # Volume for Let's Encrypt certificates
command:
# EntryPoints
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http.tls=true"
- "--certificatesresolvers.letsencrypt.acme.email=youremail@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
-
- "--providers.file.directory=/dynamic" # File provider — watches the whole /dynamic directory for *.yaml changes
- "--providers.file.watch=true"
- "--api.insecure=false" # Disable the insecure API dashboard. In production, you should secure the API dashboard with authentication and HTTPS.
Add the dynamic configuration as volume. In this files you declare your app routes and domain name.
http:
routers:
app:
rule: "Host(`yourdomain.com`)"
service: app
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
app:
loadBalancer:
servers:
- url: "http://app:8000"
Also create a certs directory and give write permissions to the acme.json file for Traefik to store the SSL certificates.
mkdir -p certs
touch certs/acme.json
chmod 600 certs/acme.json
After setting up the configuration, you can run the application using Docker Compose.