diff --git a/docs/nginx_photos b/docs/nginx_photos
index 243653eb74efb5cbe578d48082ceb21fd90feb23..77454b48fc0726764e8b02b524afca19c37aac60 100644
--- a/docs/nginx_photos
+++ b/docs/nginx_photos
@@ -34,8 +34,14 @@ server {
     error_log /var/log/nginx/photos.crans.org_error.log;
     access_log /var/log/nginx/photos.crans.org_access.log;
 
+    # Allow 2Go upload at once
+    client_max_body_size 2G;
+
     # Django statics and media
-    location /media  {
+    # Do not directly serve media, it must be authorized
+    # by a Django view to check permissions
+    location /protected/media  {
+        internal;
         alias /var/www/photos/photo21/media;
     }
     location /static {
diff --git a/photo21/urls.py b/photo21/urls.py
index d3ffe9deabf665ca64fa99b918d11bfd0f41951d..3b6c278578717d82e40430a06382c91cf82961ab 100644
--- a/photo21/urls.py
+++ b/photo21/urls.py
@@ -14,11 +14,11 @@ Including another URLconf
     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 """
 from django.contrib import admin
-from django.urls import include, path
+from django.urls import include, path, re_path
 from django.conf import settings
 from django.conf.urls.static import static
 
-from .views import IndexView
+from .views import IndexView, MediaAccess
 
 # photologue_custom overrides some photologue patterns
 urlpatterns = [
@@ -33,3 +33,5 @@ urlpatterns = [
 
 if settings.DEBUG:
     urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+else:
+    urlpatterns.append(re_path('^media/(?P<path>.*)', MediaAccess.as_view(), name='media'))
diff --git a/photo21/views.py b/photo21/views.py
index 0533c20fea834f4b18be343c66f20e1b181f177f..90f4b55d06016e9c0f487894beb3dbd30273a12a 100644
--- a/photo21/views.py
+++ b/photo21/views.py
@@ -2,10 +2,20 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from django.contrib.auth.mixins import LoginRequiredMixin
-from django.views.generic import ListView
+from django.views.generic import ListView, View
+from django.http import HttpResponse
 from photologue.models import Gallery
 
 
+class MediaAccess(LoginRequiredMixin, View):
+    def get(self, request, path):
+        response = HttpResponse()
+        # Content-type will be detected by nginx
+        del response['Content-Type']
+        response['X-Accel-Redirect'] = '/protected/media/' + path
+        return response
+
+
 class IndexView(LoginRequiredMixin, ListView):
     queryset = Gallery.objects.on_site().is_public()
     paginate_by = 4