If you are hosting a Django app with Kubernetes, there are a few things that you need to take care of.
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.
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=
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;
}
}
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
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
Build your containers and run kubectl -f apply
on each of the kubernetes files you made.