Repository: cookiecutter/cookiecutter-django
Pull request: Add option to serve media files locally using nginx
## Description

This PR proposes an example solution to issue #2449.
It allows selecting a local nginx instance to serve media files.

This is just a bare bones implementation that could serve as a base for serving media files. 

I added an nginx container. This container has a volume mapped where django stores its media files. Routes and services are added in the traefik configuration to route `{{domain.name}}/media/*`
requests to the nginx instance which serves them.

## Rationale

It makes deploying very simple and small scale projects easier as @demestav eloquently expressed in his issue.

## Use case(s) / visualization(s)
When using AWS or GCP is not a good option. Serve the `/media` files using nginx.

```
/media/ --> nginx -----------|
django ---> uwsgi -----------o--> user
/static/ -> whitenoise ------|
```

Fix #2449
Closes #3260 

Problem statement:
Serving media files without an external service
## Description
Currently, in order to serve media files, you need an external file storage service. Setting up an external file storage service introduces some complexity (especially for people who do not already use these services), as well as potentially extra costs. Even though it is a proper way to serve files in production, I believe it is not a requirement for all the use-cases that, in my opinion, are within the scope of cookiecutter-django. Deployment of small-scale projects could benefit by a simpler configuration.

## Proposition
I propose to have an option to serve media files without an external service.

We are using Traefik as a reverse proxy. Traefik does not serve files (by design) therefore a web server is needed to do this job. To achieve this, we can have an Nginx behind Traefik to handle the media files route **only**.

## Comments
- The trade-off is that we add yet another docker resource in the project, making it the complete system bigger (maintenance, resources, build time etc.)
- Nginx is a beast of its own
- Someone might argue why not use Nginx exclusively (why use Traefik); that was the state previously but the project switched to Caddy and later to Traefik, and part of the reason was because it is easier and safer to get automatic SSL certificates
- I am currently testing this setup with a small project and so far seems OK.

## Closing
Would the maintainers be interested to add this? Are there any major disadvantages that I am missing?

Hints from issue discussion:
Hello how about just setting up whitenoise to serve your files? Its more simpler as compared to coming up with an inbuilt configuration.
Are we talking about static files or media files? These are 2 different things, static files are part of your code while media files are more the one that are created in the lifetime of your app (user uploaded content, generated PDF, etc...). Whitenoise [is not suitable for media files](http://whitenoise.evans.io/en/stable/django.html#serving-media-files). 
@Labohkip81 exactly what browniebroke said. Further, cookiecutter-django already provides the option to use whitenoise to serve static files.

@browniebroke I am talking about media files.
I own a very small website where I opted to use nginx to serve the media files. The additional configuration is not very hard to set up. It's like 10 lines in the docker compose and another 10 for the nginx config.
I went ahead and tried to implement an example of how this can be integrated. The way I use Traefik differs from the repo style so I adapted my approach to better suit this codebase. Hopefully this can be used as a starting point to see whether adding this feature or even expanding it is feasible.
I'm running into this same problem now. Trying to deploy a production instance for a university project, and not trying to deal with CDN for the setup we have. 

Tried @ArkadiuszMichalRys's fork, but getting several errors: https://github.com/pydanny/cookiecutter-django/pull/2457#issuecomment-597281545



I have updated the pull request to include a few fixes. The generated project now works when I test it.
You will want to choose option 3 (nginx) for the cloud provider and enable whitenoise.

I am not sure about a use case where nginx would be used without whitenoise but I left the option in there in case I am missing something.
Can the existing AWS S3 be configure to work with [minio](https://min.io/)?
I applied the pull request manually.

It works, but the development server reloads when it detects uploaded files.

I don't see a way to exclude a directory: https://www.uvicorn.org/settings/
@OmarWKH the method I describe in this issue is for **production only**. In development, files are served by the development server; you don't need to use NGINX.

Commit message: Use nginx to serve media files if cloud provider is None

Patch to review:
diff --git a/README.md b/README.md
index 0697862c5b..8eb3fbe19c 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ production-ready Django projects quickly.
 -   Optional basic ASGI setup for Websockets
 -   Optional custom static build using Gulp or Webpack
 -   Send emails via [Anymail](https://github.com/anymail/django-anymail) (using [Mailgun](http://www.mailgun.com/) by default or Amazon SES if AWS is selected cloud provider, but switchable)
--   Media storage using Amazon S3, Google Cloud Storage or Azure Storage
+-   Media storage using Amazon S3, Google Cloud Storage, Azure Storage or nginx
 -   Docker support using [docker-compose](https://github.com/docker/compose) for development and production (using [Traefik](https://traefik.io/) with [LetsEncrypt](https://letsencrypt.org/) support)
 -   [Procfile](https://devcenter.heroku.com/articles/procfile) for deploying to Heroku
 -   Instructions for deploying to [PythonAnywhere](https://www.pythonanywhere.com/)
diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py
index b64bbbafdb..23ab41f5a6 100644
--- a/hooks/post_gen_project.py
+++ b/hooks/post_gen_project.py
@@ -492,10 +492,6 @@ def main():
         )
 
     if "{{ cookiecutter.cloud_provider }}" == "None":
-        print(
-            WARNING + "You chose not to use a cloud provider, "
-            "media files won't be served in production." + TERMINATOR
-        )
         remove_storages_module()
 
     if "{{ cookiecutter.use_celery }}".lower() == "n":
diff --git a/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile
new file mode 100644
index 0000000000..911b16f717
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile
@@ -0,0 +1,2 @@
+FROM nginx:1.17.8-alpine
+COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf
diff --git a/{{cookiecutter.project_slug}}/compose/production/nginx/default.conf b/{{cookiecutter.project_slug}}/compose/production/nginx/default.conf
new file mode 100644
index 0000000000..aafdd5beea
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/compose/production/nginx/default.conf
@@ -0,0 +1,7 @@
+server {
+	listen       80;
+	server_name  localhost;
+	location /media/ {
+		alias /usr/share/nginx/media/;
+	}
+}
diff --git a/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml b/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml
index cc183cd6cf..ea57f4a51f 100644
--- a/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml
+++ b/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml
@@ -57,6 +57,18 @@ http:
         # https://docs.traefik.io/master/routing/routers/#certresolver
         certResolver: letsencrypt
     {%- endif %}
+    {%- if cookiecutter.cloud_provider == 'None' %}
+
+    web-media-router:
+      rule: "Host(`{{ cookiecutter.domain_name }}`) && PathPrefix(`/media/`)"
+      entryPoints:
+        - web-secure
+      middlewares:
+        - csrf
+      service: django-media
+      tls:
+        certResolver: letsencrypt
+    {%- endif %}
 
   middlewares:
     csrf:
@@ -77,6 +89,13 @@ http:
         servers:
           - url: http://flower:5555
     {%- endif %}
+    {%- if cookiecutter.cloud_provider == 'None' %}
+
+    django-media:
+      loadBalancer:
+        servers:
+          - url: http://nginx:80
+    {%- endif %}
 
 providers:
   # https://docs.traefik.io/master/providers/file/
diff --git a/{{cookiecutter.project_slug}}/production.yml b/{{cookiecutter.project_slug}}/production.yml
index 45c8b3c1dd..4e9b9e048d 100644
--- a/{{cookiecutter.project_slug}}/production.yml
+++ b/{{cookiecutter.project_slug}}/production.yml
@@ -4,6 +4,9 @@ volumes:
   production_postgres_data: {}
   production_postgres_data_backups: {}
   production_traefik: {}
+  {%- if cookiecutter.cloud_provider == 'None' %}
+  production_django_media: {}
+  {%- endif %}
 
 services:
   django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
@@ -24,6 +27,10 @@ services:
       {%- endif %}
 
     image: {{ cookiecutter.project_slug }}_production_django
+    {%- if cookiecutter.cloud_provider == 'None' %}
+    volumes:
+      - production_django_media:/app/{{ cookiecutter.project_slug }}/media
+    {%- endif %}
     depends_on:
       - postgres
       - redis
@@ -89,3 +96,14 @@ services:
     volumes:
       - production_postgres_data_backups:/backups:z
   {%- endif %}
+  {%- if cookiecutter.cloud_provider == 'None' %}
+  nginx:
+    build:
+      context: .
+      dockerfile: ./compose/production/nginx/Dockerfile
+    image: {{ cookiecutter.project_slug }}_local_nginx
+    depends_on:
+      - django
+    volumes:
+      - production_django_media:/usr/share/nginx/media:ro
+  {%- endif %}


Code review comments:
diff_hunk: @@ -492,10 +492,6 @@ def main():
         )
 
     if "{{ cookiecutter.cloud_provider }}" == "None":
-        print(
-            WARNING + "You chose not to use a cloud provider, "
-            "media files won't be served in production." + TERMINATOR
-        )
path: hooks/post_gen_project.py
text: @user1:
I've removed that but actually we should probably leave this warning if not using docker.

@user2:
Thank you @user1. We have been waiting for this to get into the `master` branch. I have maintained my own branch based on @author work. Let me see what we can add to it, especially in the documentation.