Skip to content

Instantly share code, notes, and snippets.

@jjorissen52
Last active May 27, 2024 10:27
Show Gist options
  • Save jjorissen52/bde86dfffc22c75a1a17c1eb056fbde7 to your computer and use it in GitHub Desktop.
Save jjorissen52/bde86dfffc22c75a1a17c1eb056fbde7 to your computer and use it in GitHub Desktop.
Django with Kubernetes

If you are hosting a Django app with Kubernetes, there are a few things that you need to take care of.

Containerizing

Here is a sample Dockerfile for a containerized app:

FROM python:3.9-buster

RUN apt-get update \
    --fix-missing \
    && rm -rf /var/lib/apt/lists/*

ENV APP_HOME /app
WORKDIR $APP_HOME

ADD requirements.txt requirements.txt
RUN pip install -r requirements.txt --no-cache-dir \
    && rm requirements.txt

COPY . .

ENV PORT 8000
CMD exec gunicorn --bind :$PORT --workers 1 --threads 4 my_app.wsgi

You will also want to configure your app to pull from the environment or a file. I like to use python-dotenv for the best of both worlds.

Config

You will need to make a kubernetes Secret, (the DATABASE_HOST setting shown below is assumed in the section on databases). This file will ultimately be mounted to /etc/env/.env, so in settings.py you will want to load the values from there.

apiVersion: v1
kind: Secret
metadata:
  name: django-app-env
type: Opaque
stringData:
  .env: |
    DEBUG=True
    USE_SQLITE=0
    ALLOWED_HOSTS=*,
    INITIAL_ADMIN_EMAIL=
    INITIAL_ADMIN_PASSWORD=
    DATABASE_NAME=
    DATABASE_USER=
    DATABASE_PASSWORD=
    DATABASE_HOST=app-db # 
    API_VERSION=

Serving Static Files

Gunicorn and other suitable WSGI servers for Django do not serve static files, so we will want to setup a reverse proxy server such as NGINX to take care of static files. In settings.py:

STATIC_ROOT = os.environ.get('STATIC_ROOT_PATH', BASE_DIR / 'static')

Then we make a Deployment and Service where a volume mount is shared between the Django app container and the NGINX container so that the app's staticfiles can be collected into a location that the NGINX container can read on initialization (initContainers) :

apiVersion: v1
kind: Service
metadata:
  name: django-app
spec:
  ports:
  - port: 8000
    targetPort: 8080
  type: LoadBalancer
  selector:
    app: django-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: django-app
  labels:
    app: django-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: django-app
  template:
    metadata:
      labels:
        app: django-app
    spec:
      volumes:
        - name: service-config
          secret:
            secretName: django-app-env
        - name: static
          emptyDir: { }
      # on container startup, move the collected staticfiles
      initContainers:
        - name: move-static
          image: django-app
          env:
            - name: STATIC_ROOT_PATH
              value: "/usr/share/nginx/static"
          volumeMounts:
            - name: static
              mountPath: /usr/share/nginx/static
          command: [ python, manage.py, collectstatic, --noinput ]
      containers:
        - name: app-nginx
          image: app-nginx
          ports:
            - containerPort: 8080
          volumeMounts:
          - name: static
            mountPath: /usr/share/nginx/static
        - name: django-app
          image: django-app
          env:
            - name: ENV_FILE_PATH
              value: "/etc/env/.env"
          volumeMounts:
          - name: service-config
            mountPath: "/etc/env"
            readOnly: true
          livenessProbe:
            httpGet:
              path: /
              port: 8000
            initialDelaySeconds: 3
          ports:
            - containerPort: 8000
          resources:
            requests:
              cpu: "1"
              memory: "1024Mi"
            limits:
              cpu: "2"
              memory: "2048Mi"

Here is a sample NGINX Dockerfile and conf:

# Dockerfile
FROM nginx:1.19
COPY nginx.conf /etc/nginx/nginx.conf
# nginx.conf
user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
error_log  /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024; # increase if you have lots of clients
  accept_mutex off; # set to 'on' if nginx worker_processes > 1
  # 'use epoll;' to enable for Linux 2.6+
  # 'use kqueue;' to enable for FreeBSD, OSX
}

http {
    upstream django_app {
        server localhost:8000;
    }

    server {
        listen 8080;

        root /usr/share/nginx;
        location / {
          # checks for static file, if not found proxy to app
          try_files $uri @proxy_to_app;
        }

        location @proxy_to_app {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
            proxy_pass http://django_app;
        }
    }

    types {
      text/html                             html htm shtml;
      text/css                              css;
      text/xml                              xml rss;
      image/gif                             gif;
      image/jpeg                            jpeg jpg;
      application/x-javascript              js;
      text/plain                            txt;
      text/x-component                      htc;
      text/mathml                           mml;
      image/png                             png;
      image/x-icon                          ico;
      image/x-jng                           jng;
      image/vnd.wap.wbmp                    wbmp;
      application/java-archive              jar war ear;
      application/mac-binhex40              hqx;
      application/pdf                       pdf;
      application/x-cocoa                   cco;
      application/x-java-archive-diff       jardiff;
      application/x-java-jnlp-file          jnlp;
      application/x-makeself                run;
      application/x-perl                    pl pm;
      application/x-pilot                   prc pdb;
      application/x-rar-compressed          rar;
      application/x-redhat-package-manager  rpm;
      application/x-sea                     sea;
      application/x-shockwave-flash         swf;
      application/x-stuffit                 sit;
      application/x-tcl                     tcl tk;
      application/x-x509-ca-cert            der pem crt;
      application/x-xpinstall               xpi;
      application/zip                       zip;
      application/octet-stream              deb;
      application/octet-stream              bin exe dll;
      application/octet-stream              dmg;
      application/octet-stream              eot;
      application/octet-stream              iso img;
      application/octet-stream              msi msp msm;
      audio/mpeg                            mp3;
      audio/x-realaudio                     ra;
      video/mpeg                            mpeg mpg;
      video/quicktime                       mov;
      video/x-flv                           flv;
      video/x-msvideo                       avi;
      video/x-ms-wmv                        wmv;
      video/x-ms-asf                        asx asf;
      video/x-mng                           mng;
    }
}

Database

You have the option to use an external database or a StatefulSet, but here we just assume you are using an external database. You will need to create a Service and Endpoint for your Django app to connect to your database. Let's say your database server is bound to 172.20.20.5:

kind: "Service"
apiVersion: "v1"
metadata:
  name: app-db
spec:
  clusterIP: None
  ports:
  - port: 5432
---
kind: "Endpoints"
apiVersion: "v1"
metadata:
  name: app-db
subsets:
  -
    addresses:
       - ip: "172.20.20.5"
    ports:
      - port: 5432
        name: app-db

Migrations

A good way to handle migrations is to run a Job:

kind: Job
apiVersion: batch/v1
metadata:
  name: migrate
spec:
  backoffLimit: 10
  template:
    spec:
      volumes:
        - name: service-config
          secret:
            secretName:  django-app-env
      containers:
        - name: migrate-app-db
          image: django-app
          command: [python, manage.py, migrate, --database, default]
          env:
            - name: ENV_FILE_PATH
              value: "/etc/env/.env"
          volumeMounts:
            - name: service-config
              mountPath: "/etc/env"
              readOnly: true

Apply

Build your containers and run kubectl -f apply on each of the kubernetes files you made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment