diff --git a/contracts/migrations/0009_alter_contractfile_file.py b/contracts/migrations/0009_alter_contractfile_file.py
new file mode 100644
index 0000000000000000000000000000000000000000..798582d85ee3d0b32f2e8a0d24362baa490905d6
--- /dev/null
+++ b/contracts/migrations/0009_alter_contractfile_file.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-22 10:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0008_alter_contracteesignaturerepresentative_options_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contractfile',
+            name='file',
+            field=models.FileField(upload_to='_private/', verbose_name='Soubor'),
+        ),
+    ]
diff --git a/contracts/models.py b/contracts/models.py
index 811b5a502dc746c7ac0f6fdee5c65f15024c5878..9fe9d08fc1a3e8b33bf0af53e02b0c59428b7de9 100644
--- a/contracts/models.py
+++ b/contracts/models.py
@@ -532,7 +532,7 @@ class ContractFile(NameStrMixin, models.Model):
 
     file = models.FileField(
         verbose_name="Soubor",
-        upload_to="private/",
+        upload_to="_private/",
     )
 
     contract = models.ForeignKey(
diff --git a/media_server/__init__.py b/media_server/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/media_server/admin.py b/media_server/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /dev/null
+++ b/media_server/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/media_server/apps.py b/media_server/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a7dc443704f73901f1122bd07754e3c65df5cda
--- /dev/null
+++ b/media_server/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MediaServerConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'media_server'
diff --git a/media_server/migrations/__init__.py b/media_server/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/media_server/models.py b/media_server/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91
--- /dev/null
+++ b/media_server/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/media_server/tests.py b/media_server/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /dev/null
+++ b/media_server/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/media_server/urls.py b/media_server/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..23bb64ae39d8b5b07f98b14e44c3cff7b6532382
--- /dev/null
+++ b/media_server/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+app_name = "media_server"
+urlpatterns = [
+    path(
+        "<path:path>",
+        views.view_media,
+        name="view_media",
+    ),
+]
diff --git a/media_server/views.py b/media_server/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..e803d5669fa1051af213c58faa18a018d9ece01b
--- /dev/null
+++ b/media_server/views.py
@@ -0,0 +1,28 @@
+import os
+
+from django.core.files.storage import FileSystemStorage
+from django_downloadview import StorageDownloadView
+from django_http_exceptions import HTTPExceptions
+
+
+# Create your views here.
+
+storage = FileSystemStorage()
+
+
+class MediaView(StorageDownloadView):
+    attachment = False
+
+    def get_path(self, *args, **kwargs) -> str:
+        path = super().get_path(*args, **kwargs)
+
+        # Make sure path is clean
+        path = os.path.normpath(path)
+
+        if path.startswith("_"):  # Private path
+            raise HTTPExceptions.NOT_FOUND
+
+        return path
+
+
+view_media = MediaView.as_view(storage=storage)
diff --git a/nginx.conf b/nginx.conf
index dd8e8f0911cdca04480f2e18a05deeef7ec191c7..4725c76f1dab2ac47717af04a9e227b6af423a30 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -20,13 +20,4 @@ server {
         proxy_set_header X-Forwarded-Proto https;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
-
-    location /media/ {
-        alias /var/opt/contract_registry/media/
-    }
-
-    location ~ /media/private/ {
-        deny all;
-        return 404;
-    }
 }
diff --git a/registry/settings/base.py b/registry/settings/base.py
index 576b301dd880b1aae1ea6fba1503856699508c86..7c54586d99d8ab0e87c858dfd2287423b7e6c9f9 100644
--- a/registry/settings/base.py
+++ b/registry/settings/base.py
@@ -64,6 +64,7 @@ INSTALLED_APPS = [
     "markdownx",
     "pirates",
     "webpack_loader",
+    "media_server",
     "shared",
     "contracts",
     "oidc",
diff --git a/registry/urls.py b/registry/urls.py
index ed9a38ad116e390037f09dc71dbce86ebfb9c4ad..c12fecc96e1c3ac179f7fcdb216051330adfaed8 100644
--- a/registry/urls.py
+++ b/registry/urls.py
@@ -28,5 +28,6 @@ urlpatterns = [
     path("", include("users.urls")),
     path("markdownx/", include("markdownx.urls")),
     path("oidc/", include("oidc.urls")),
+    path("media/", include("media_server.urls")),
     path("admin/", admin.site.urls),
 ] + pirates_urlpatterns