Docker + Django + nginx + gunicorn

Guys, who can help set up Docker + Django + Gunicorn + Nginx?
I get an error "/etc/nginx/html/index.html" is not found (2: No such file or directory)
Or any other path

My code on github

Hey there!
I recently done this setup on docker.
I used traefik as a reverse-proxy to django (gunicorn) / nginx and others services. Traefik makes it a lot easier to configure services with docker. And on my projects i only nginx as a static files server (i don’t find nginx configurations “friendly”).

So, here’s my setup for development (HTTP only). I use PDM as dependency/project manager, so the dockerfile below is suited for this.

# Dockerfile

FROM python:3.9

# set work directory
WORKDIR /app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

RUN apt update -y
RUN apt install -y netcat gettext

RUN pip install pip setuptools wheel
RUN pip install pdm==2.1.0

COPY pyproject.toml pdm.lock manage.py /app/
# Install dev deps
RUN pdm install --no-lock --no-editable

COPY entrypoint.api.sh entrypoint.api.sh
RUN sed -i 's/\r$//g' entrypoint.api.sh
RUN chmod +x  entrypoint.api.sh

ENV PYTHONPATH=src:.venv/lib/python3.9/site-packages
COPY ./ /app

ENTRYPOINT [ "sh", "/app/entrypoint.api.sh" ]

# docker-compose.yml

version: "3.3"
services:
  traefik:
    build:
      dockerfile: traefik.Dockerfile
      context: .
    command:
      - "--configFile=traefik.yml"
      - "--providers.docker=true"
    ports:
      - "80:80"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./traefiklogs:/traefiklogs"
  app:
    build: .
    volumes:
      - .:/app/
      - staticfiles:/app/static
    ports:
      - 80
    env_file:
      - .env.dev
    labels:
      traefik.enable: true
      traefik.http.routers.api.rule: Host(`${HOST}`)
      traefik.http.routers.api.entrypoints: http
    command: python3 manage.py runserver 0.0.0.0:80
    depends_on:
      - traefik
  nginx:
    depends_on:
      - traefik
      - app
    image: nginx:alpine
    expose:
      - 80
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - staticfiles:/app/static
    labels:
      traefik.enable: "true"
      traefik.http.routers.nginx.rule: Host(`${HOST}`) && PathPrefix(`/static`)
      traefik.http.routers.nginx.entrypoints: http
      traefik.http.services.nginx.loadbalancer.server.port: "80"

volumes:
  staticfiles:
  .:
# traefik.Dockerfile
FROM traefik:v2.4

COPY traefik.yml /etc/traefik/
# nginx.conf
worker_processes 1;

error_log  /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024; # increase if you have lots of clients
}

http {
  include mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  access_log /dev/null;
  sendfile on;


  server {
    # if no Host match, close the connection to prevent host spoofing
    listen 80 default_server;
    return 444;
  }

  server {
    listen 80;
    client_max_body_size 4G;

    server_name dev.boilerplate.com;

    keepalive_timeout 5;

    # path for static files
    root /app/static;

    location /static {
      # checks for static file
      alias /app/static;
    }

  }
}
# traefik.yml
global:
  sendAnonymousUsage: false

providers:
  docker: {}

entryPoints:
  http:
    address: ":80"

  https:
    address: ":443"

certificatesResolvers:
  ssl:
    acme:
      httpChallenge:
        entryPoint: http
      email: levoagoraapp@gmail.com
      storage: /letsencrypt/acme.json

accessLog:
  filePath: "/traefiklogs/access.log"
  format: json
  fields:
    defaultMode: drop
    names:
      ClientHost: keep
      DownstreamStatus: keep
      DownstramContentSize: keep
      OriginDuration: keep
      OriginStatus: keep
      RequestMethod: keep
      RequestPath: keep
      RequestProtocol: keep
      ServiceName: keep
      StartUTC: keep

    headers:
      defaultMode: drop
      names:
        User-Agent: keep
        Content-Type: keep

Basically, this configuration tells traefik to listen on a given HOST environment variable. This variable is set on my .env.dev file. Whenever a incoming request has a path /static this request is forwarded to nginx service that serves the static files, otherwise is forwarded to the app service. Nginx receives the static files from a volume shared with the app service. This directory is filled up with the static files with the collectstatic management command that is run on the app entrypoint. Here is the entrypoint:

# entrypoint.api.sh
#!/bin/sh
set -e

if [ "$DJANGO_SETTINGS_MODULE" = "app.settings.dev" ]
then

    while ! nc -z $SQL_HOST $SQL_PORT; do
        echo "Waiting for postgres... $SQL_HOST $SQL_PORT"
        sleep 5
    done

    echo "PostgreSQL started"
fi

python manage.py migrate
python manage.py collectstatic --noinput
python manage.py compilemessages -v 0

exec "$@"

After this, nginx contains all the static files to serve.

All of this code is available at github, it comes from my boilerplate project. I use this strategy when deploying apps on the cloud using virtual machines.

Let me know what you think about this!

Looks great, but I’d like to deal with Docker + Django + nginx + gunicorn. Without add-on services