Django, Postgres, Gunicorn, Nginx with Docker (Part-2)

Posted on June 4, 2021, 1:45 p.m., 1150, by: sagar



Gunicorn

Now, install Gunicorn. It's production grade WSGI server.

For now, since we want to use default django's built-in server, create production compose file:


version: '3.5'

services:
    app:
        build:
            context: .
        command: gunicorn personal.wsgi:application --bind 0.0.0.0:8000
        volumes:
            - static_data:/vol/static
        ports:
            - "8000:8000"
        restart: always
        env_file:
            - .env.prod
        depends_on:
            - app-db

    app-db:
        image: postgres:12-alpine
        ports:
            - "5432:5432"
        restart: always
        volumes:
            - postgres_data:/var/lib/postgresql/data:rw
        env_file:
            - .env.prod
volumes:
    static_data:
    postgres_data:


Here, we're using commang gunicorn instead of django server command. we can static_data volume as it's not needed in production. For now, let's create .env.prod file for environemental variables:


DEBUG=0
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
DB_ENGINE=django.db.backends.postgresql_psycopg2
POSTGRES_HOST_AUTH_METHOD=trust
POSTGRES_USER=sagar
POSTGRES_PASSWORD=********
POSTGRES_DB=portfolio_db_prod
POSTGRES_HOST=app-db
POSTGRES_PORT=5432


Add both files to .gitignore file if you want to keep them out from version control. Now, down all containers with -v flag, -v flag removes associated volumes:

$ docker-compose down -v
Then, re-build images and run the containers:
$ docker-compose -f docker-compose.prod.yml up --build
Run with -d flag if you wan't to run services in background. If any error when running, check errors with command:
$ docker-compose -f docker-compose.prod.yml logs -f
Wow, let's create production Dockerfile as Dockerfile.prod with production entrypoint.prod.sh file inside scripts directory of the root. entrypoint.prod.sh script file:


#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

exec "[email protected]"


Dockerfile.prod file with scripts permission:


FROM python:3.8.9-alpine as builder


ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONNUNBUFFERED 1

RUN apk update
RUN apk add postgresql-dev gcc python3-dev musl-dev libc-dev linux-headers

RUN apk add jpeg-dev zlib-dev libjpeg

RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt



#### FINAL ####

FROM python:3.8.9-alpine

RUN mkdir /app
COPY . /app
WORKDIR /app

RUN apk update && apk add libpq
COPY --from=builder ./wheels /wheels
COPY --from=builder ./requirements.txt .
RUN pip install --no-cache /wheels/*
#RUN pip install -r requirements.txt


COPY ./scripts /scripts
RUN chmod +x /scripts/*

RUN mkdir -p /vol/media
RUN mkdir -p /vol/static

#RUN adduser -S user

#RUN chown -R user /vol

RUN chmod -R 755 /vol
#RUN chown -R user /app
#RUN chmod -R 755 /app

#USER user

ENTRYPOINT ["/scripts/entrypoint.prod.sh"]


Here we used multi-stage build as it reduces final image size. 'builder' is temporary image that's used just to build python wheels with dependencies, that is copied to Final stage. we can create non-root user. Because that is the best practice to be safe from attackers. Now, update the compose production file with docker production file:


version: '3.5'

services:
    app:
        build:
            context: .
            dockerfile: Dockerfile.prod
        command: gunicorn personal.wsgi:application --bind 0.0.0.0:8000
        volumes:
            - static_data:/vol/static
        expose:
            - "8000:8000"
        restart: always
        env_file:
            - .env.prod
        depends_on:
            - app-db

    app-db:
        image: postgres:12-alpine
        ports:
            - "5432:5432"
        restart: always
        volumes:
            - postgres_data:/var/lib/postgresql/data:rw
        env_file:
            - .env.prod
volumes:
    static_data:
    postgres_data:


Rebuild, and run:

$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec app python manage.py migrate --noinput


Ngnix

Nginx, really gives you the ultimate power. You can do whatever you want. Let's add nginx to act as reverse proxy for Gunicorn. Add service on docker compose file (production):
version: '3.5'

services:
    app:
        build:
            context: .
            dockerfile: Dockerfile.prod
        command: gunicorn personal.wsgi:application --bind 0.0.0.0:8000
        volumes:
            - static_data:/vol/static
            - media_data: /vol/media
        ports:
            - "8000:8000"
        restart: always
        env_file:
            - .env.prod
        depends_on:
            - app-db

    app-db:
        image: postgres:12-alpine
        ports:
            - "5432:5432"
        restart: always
        volumes:
            - postgres_data:/var/lib/postgresql/data:rw
        env_file:
            - .env.prod

    proxy:
        build: ./proxy
        volumes:
            - static_data:/vol/static
            - media_data:/vol/media
        restart: always
        ports:
            - "8008:80"
        depends_on:
            - app
volumes:
    static_data:
    media_data:
    postgres_data:


Inside root directory create a proxy(whatever you want to name it) directory and add a configuration file, in my case I have created default.conf file as:


server {
    listen 80;

    location /static {
        alias /vol/static;
    }

    location /media {
        alias /vol/media;
    }


    location / {
        uwsgi_pass app:8000;
        include /etc/nginx/uwsgi_params;
    }
}


And create uwsgi_params file for this.

uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;


Also add a Dockerfile inside proxy directory for nginx configuration:

FROM nginxinc/nginx-unprivileged:1-alpine

COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY uwsgi_params /etc/nginx/uwsgi_params


You can use expose instead of ports in docker-compose.prod.yml file for app service:

    app:
        build:
            context: .
            dockerfile: Dockerfile.prod
        command: gunicorn personal.wsgi:application --bind 0.0.0.0:8000
        volumes:
            - static_data:/vol/static
            - media_data:/vol/media
        expose:
            - 8000
        restart: always
        env_file:
            - .env.prod
        depends_on:
            - app-db


Again, re-build run and try:

$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Ensure app is running in http://localhost:8008.

That's it.


Write your comment

0 comments

No comments yet. 😀