Django 3: Serve Django static files with NGINX
Tags: django, python, docker, nginx
Published: 2024-03-06 06:18:52

> _NOTE: This article was initially posted on my Substack, at https://andresalvareziglesias.substack.com/_

Hi everyone!

In the first post of this Python/Django/Docker tutorial series, we define an idea to test these technologies with a real application: a supercool version of Tic Tac Toe with magical gems, and dragons, and a supersmart CPU player that uses IA. And sparks. A lot of sparks.

The second step in the basic project architecture is to deal with static files, because Gunicorn is a pure python server, so we need another server for this purpose.

Let's get started!

## Articles in this series

- Chapter 1: [Let the journey start](https://andresalvareziglesias.substack.com/p/let-the-journey-start-with-python-docker-and-ai?utm_source=post-toc)
- Chapter 2: [Create a containerized Django app with Gunicorn and Docker](https://andresalvareziglesias.substack.com/p/create-a-containerized-django-app?utm_source=post-toc)
- Chapter 3: [Serve Django static files with NGINX](https://andresalvareziglesias.substack.com/p/serve-django-static-files-with-nginx?utm_source=post-toc)
- Chapter 4: [Adding a database to our stack](https://andresalvareziglesias.substack.com/p/adding-a-database-to-our-stack?utm_source=post-toc)
- Chapter 5: [Applications and sites](https://andresalvareziglesias.substack.com/p/django-5-applications-and-sites)
- Chapter 6: [Using the Django ORM](https://andresalvareziglesias.substack.com/p/django-6-using-the-django-orm)
- Chapter 7: [Users login, logout and register](https://andresalvareziglesias.substack.com/p/django-7-users-login-logout-and-register)

## About Django static files

Every Django application has dynamic content (the Python part) and static content (images, CSS and JS files, etc).

To use the static files in our project, first we need to edit settings.py inside Django app source code folder, and set STATIC_URL and STATIC_ROOT variables:

```
STATIC_URL = 'static/'
STATIC_ROOT = '../static'
```

Then, download the static files with the manage.py command in app folder.:

```
cd tic-magical-line/app/src/ticmagicalline
python3 manage.py collectstatic
```

The static files have been downloaded to app/src/static folder. You can use a file browser to check them all.


![Django app folder structure with static files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8yxzoptz6azmzy130mf.png)

As you can see, these are the static files (CSS, JS and images) of the Django admin site.

## Create a NGINX container to serve the static files

As we said before, Gunicorn only serves dynamic python files, so we will use NGINX to serve the static files and to give our app a single entry point.

First, we will create a dedicated folder for the server at the root of the project:

```
cd tic-magical-line
mkdir server
mkdir server/static
```

Inside the server folder, create the same three files as before to define the container:

```
cd tic-magical-line/server
touch Dockerfile
touch environment.env
touch requirements.txt
```

Leave environment.env and requirements.txt empty. Write the following content in Dockerfile:

```
FROM nginx:alpine
ADD ./server/default.conf /etc/nginx/conf.d
ADD ./app/src/static /var/www/html/static
CMD [ "nginx", "-g", "daemon off;" ]
```

In the line ADD ./app/src/ticmagicalline/static /var/www/html/static, we are including the static content from the app folder. 

Now, create the NGINX server configuration:

```
cd tic-magical-line/server
touch default.conf
```

With this content:

```
upstream ticmagicalline {
   server 10.20.30.1:8081;
}

server {
   listen 8080;

   location / {
       proxy_pass http://10.20.30.1:8081;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header Host $host;
       proxy_redirect off;
   }

   location /static/ {
       alias /var/www/html/static/;
   }
}
```

These IPs will be each machine IP in our inner docker network, keep reading to know more.

IMPORTANT! We will change the initial port chosen in the Django app from 8080 to 8081, to allow NGINX to serve the app in the port 8080. We can also remove the exposed port in Django Dockerfile, because it is not needed anymore. And we will make a minor change in paths to Dockerfile of the Django app, to change the base path used in docker-compose build phase. You can see al the changes in the project public repository.

## About traffic origins in the internal Docker network

Now the traffic to Gunicorn is redirecter internally by NGINX, as defined in these lines:

```
upstream ticmagicalline {
   server 10.20.30.1:8081;
}
```

We need to define the NGINX internal IP as an allowed domain in the Django app. Edit settings.py inside Django app source code folder, and set CSRF_TRUSTED_ORIGINS variable:

```
CSRF_TRUSTED_ORIGINS = [
   'http://10.20.30.2:8080',
   'http://localhost:8080',
   'http://127.0.0.1:8080'
]
```

## Glue them all with docker-compose

To join these two machines (the Gunicorn server with our Django app and the NGINX server with the static content), we will write a docker-compose file, that is a definition of a more complex system composed by several containers.

To write this docker-compose file, go to the root of the project and create the compose file:

```
cd tic-magical-line
touch docker-compose.yml
```

Write this content inside:

```
version: '3.8'

services:

 app:
   build:
     context: ./app/
     dockerfile: ./app/Dockerfile
   container_name: app
   hostname: app
   restart: always
   volumes:
     - ./app/src/:/usr/src/app/
   depends_on:
     - server
   env_file:
     - ./app/environment.env
   networks:
     django_net:
       ipv4_address: 10.20.30.1

 server:
   build:
     context: ./
     dockerfile: ./server/Dockerfile
   container_name: server
   hostname: server
   restart: always
   env_file:
     - ./server/environment.env
   ports:
     - 8080:8080
   networks:
     django_net:
       ipv4_address: 10.20.30.2

networks:

 django_net:
   ipam:
     config:
       - subnet: 10.20.30.0/24
         gateway: 10.20.30.254
```

Now, we can build all the images:

```
docker compose build
```

After the build phase ends, the system is ready for testing. Run all containers with:

```
docker-compose up
```

If all goes as desired, we will see the output of the two containers, and our Django app will be accessible in http://127.0.0.1:8080, as before.

## About the list

Among the Python and Docker posts, I will also write about other related topics (always tech and programming topics, I promise... with the fingers crossed), like:

- Software architecture
- Programming environments
- Linux operating system
- Etc.

If you found some interesting technology, programming language or whatever, please, let me know! I'm always open to learning something new!

## About the author

I'm Andrés, a full-stack software developer based in Palma, on a personal journey to improve my coding skills. I'm also a self-published fantasy writer with four published novels to my name. Feel free to ask me anything!