Deploying Django app with static files using Docker and Nginx proxy
Published on 2023-08-01
This task is more complicated than I first thought it would be. For someone accustomed to Docker and all its intricacies, it probably wouldn't be so much, but here we are. Before anything, here are the two main docker-compose files used to achieve this setup; and this setup is what is being used to serve this website as of now.
first is the acme/nginx-proxy Here a example of the docker-compose for the proxy:
# docker-compose-nginx-proxy.yml
---
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- /root/docker/nginx/htpasswd:/etc/nginx/htpasswd
- /root/docker/nginx/certs:/etc/nginx/certs
- /root/docker/nginx/vhost:/etc/nginx/vhost.d
- /root/docker/nginx/html:/usr/share/nginx/html
networks:
- proxy
nginx-proxy-acme:
image: nginxproxy/acme-companion
volumes_from:
- nginx-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /root/docker/nginx/acme:/etc/acme.sh
environment:
- DEFAULT_EMAIL=me@rafaelmc.net
networks:
- proxy
networks:
proxy:
The second one is the one taking care of django, and serving the static files.
# docker-compose.yaml
---
version: "3"
services:
web:
image: rfmc28/rafaelmc.net:latest
command: bash -c "python manage.py migrate && python manage.py collectstatic --no-input && gunicorn rafaelmc.wsgi -b 0.0.0.0:8000"
container_name: rafaelmc
volumes:
- sqlite_data:/app/sqlite_data/
- /root/sqlite_backups/:/sqlite_backups/
- /root/docker/envfiles/rafaelmc.net.env:/app/.env
- static_files:/app/static/
expose:
- "8000"
environment:
- VIRTUAL_HOST=rafaelmc.net # www.rafaelmc.net
- VIRTUAL_PORT=8000
- LETSENCRYPT_HOST=rafaelmc.net # www.rafaelmc.net
- VIRTUAL_PATH=/
# - VIRTUAL_DEST=/static
networks:
- proxy
static:
image: nginx
expose:
- "80"
environment:
- VIRTUAL_HOST=rafaelmc.net #,www.rafaelmc.net
- VIRTUAL_PORT=80
# - LETSENCRYPT_HOST=rafaelmc.net #,www.rafaelmc.net
- VIRTUAL_PATH=/static/
- VIRTUAL_DEST=/
volumes:
- static_files:/usr/share/nginx/html/
networks:
- proxy
depends_on:
- web
networks:
proxy:
name: nginx-proxy_proxy
volumes:
sqlite_data:
static_files:
I think it's better to already lay out all the files, and go on a little more detail on whys, and hows.
I think the first thing to note is that I'm separating this into two different
docker-compose files because I'm running those on Portainer, so each compose
file will be it's own stack
.
Portainer is entirely optional for the task, and it's possible and easy to
combine those two compose files into one.
The most critical—and challenging to decipher from the documentation—are the
VIRTUAL_PATH
and VIRTUAL_DEST
variables defined on the django compose.
Although these variables are documented on the nginx-proxy
image,
understanding their meaning can be tricky if you're not familiar with Docker and
Nginx terminology, at least it was for me.
Let's examine the web container, which is designed to serve the entire website,
except for everything that resides in /static. The VIRTUAL_PATH
for the web is
/
, meaning it serves everything. If you don't split your Nginx proxy into
multiple containers you will never need to set this, and everything will operate
as expected.
Next, we have the static container—a basic Nginx container. This one contains
both VIRTUAL_PATH
and VIRTUAL_DEST
env variables. Here, VIRTUAL_PATH
is
equivalent to defining a location /static
in nginx.conf
.
While nginx can smartly interpret //
as the root, it might be confusing for
those like myself. To clear this up, I've defined VIRTUAL_DEST=/
, which means
nginx will receive the requests on the path relative as if they were on the root
/
.
For instance, let's say we have a file, example.com/static/css/style.css, and
this is the complete path. This file will be routed to the static container, as
it's part of /static/
. Upon reaching the static container, the request appears
as if it were on root/css/style.css
.
The final point to note is the structure of the static_files
volume. A
static_files
volume is declared and used by both the static nginx and web
containers. When we mount the volume, it will initially be empty.
However, with the command defined on the web container -
python admin.py collectstatic --no-input
- it copies the necessary files there.
Consequently, all the static files will become accessible to both containers,
the downside is that to be able to achieve some sort of automation, you need to
run collecstatic
for every deployment, even if you didn't change anything. It
shouldn't be a problem, for this simple website it's very fast on my local
machine:
(.venv)rmc@mercury.local ~/code/python/rafaelmc % time ./manage.py collectstatic --no-input [master|…1]
0 static files copied to '/Users/rmc/code/python/rafaelmc/staticfiles', 132 unmodified.
./manage.py collectstatic --no-input 0.14s user 0.04s system 95% cpu 0.186 total
(.venv)rmc@mercury.local ~/code/python/rafaelmc %
Take a look on the repository