diff --git a/etc/nginx.site.example b/etc/nginx.site.example
new file mode 100644
index 0000000000000000000000000000000000000000..1c3191ff67f46cd357c7a5f629745debfbccbb4f
--- /dev/null
+++ b/etc/nginx.site.example
@@ -0,0 +1,37 @@
+server {
+	listen 195.113.20.177:443 ssl;
+	listen [2001:718:1e03:801::b1]:443 ssl;
+
+	server_name mo.mff.cuni.cz;
+
+	ssl on;
+	ssl_certificate /etc/ssl/domains/mo.mff.cuni.cz/bundle.pem;
+	ssl_certificate_key /etc/ssl/domains/mo.mff.cuni.cz/privkey.pem;
+	ssl_dhparam /etc/ssl/dhparams.pem;
+	ssl_session_cache shared:SSL:10m;
+	ssl_session_timeout 5m;
+
+	client_max_body_size 1G;
+
+	### Production instance of OSMO
+
+	location /osmo {
+		include uwsgi_params;
+		uwsgi_pass unix:/akce/mo/osmo/var/osmo.sock;
+	}
+
+	location /osmo/static/ {
+		alias /akce/mo/osmo/static/;
+	}
+
+	location /osmo/assets/ {
+		location ~ ^/osmo/assets/[^/]+/(.*) {
+			alias /akce/mo/osmo/static/$1;
+		}
+		return 404;
+	}
+
+	location = /osmo/favicon.ico {
+		alias /akce/mo/osmo/static/favicon.ico;
+	}
+}
diff --git a/etc/uwsgi.ini.example b/etc/uwsgi.ini.example
index 9b9388b0648206dcde8bb76c9d6b75f017e9a988..86a984570316ff984b6e631ceedacbefe2d3df64 100644
--- a/etc/uwsgi.ini.example
+++ b/etc/uwsgi.ini.example
@@ -23,4 +23,5 @@ virtualenv = venv
 manage-script-name = true
 mule
 
-static-map = /static=static
+# Můžeme také pomocí uwsgi servírovat static. Lepší je to dělat Nginxem.
+# static-map = /static=static
diff --git a/mo/ext/__init__.py b/mo/ext/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..76dd78e636ddca047cdfcf7569b7edee6ffc8f0f
--- /dev/null
+++ b/mo/ext/__init__.py
@@ -0,0 +1,3 @@
+# mo.ext does nothing by itself
+
+pass
diff --git a/mo/ext/assets.py b/mo/ext/assets.py
new file mode 100644
index 0000000000000000000000000000000000000000..97a9bcdd5e9c1b189125b8035a9c46efd75b704f
--- /dev/null
+++ b/mo/ext/assets.py
@@ -0,0 +1,62 @@
+# Flask extension for versioned assets
+
+from flask import send_from_directory
+import hashlib
+import os
+from typing import Sequence, Dict
+
+
+class Assets:
+
+    asset_dir: str
+    asset_dict: Dict[str, str]
+    url_prefix: str
+
+    def __init__(self, app, url_prefix: str, asset_dir: str):
+        self.app = app
+        if app is not None:
+            self.init_app(app, url_prefix, asset_dir)
+
+    def init_app(self, app, url_prefix: str, asset_dir: str):
+        self.asset_folder = asset_dir
+        self.asset_dict = {}
+        self.url_prefix = url_prefix
+        self.app = app
+        app.jinja_env.globals.update(asset_url=lambda name: self.asset_url(name))
+        app.assets = self
+
+        # This is usually needed only for development, production requests are handled by upstream proxy
+        app.add_url_rule(
+            url_prefix + "/<version>/<path:name>",
+            endpoint="assets",
+            view_func=lambda version, name: self.send_asset(name),
+        )
+
+    def add_asset(self, name: str):
+        if name in self.asset_dict:
+            return
+
+        file_name = os.path.join(self.asset_folder, name)
+        digest = hashlib.sha1()
+
+        with open(file_name, 'rb') as file:
+            while True:
+                block = file.read(4096)
+                if not block:
+                    break
+                digest.update(block)
+
+        version = digest.hexdigest()[:8]
+        self.app.logger.debug(f'Assets: Loaded {name}: version {version}')
+        self.asset_dict[name] = version
+
+    def add_assets(self, names: Sequence[str]):
+        for name in names:
+            self.add_asset(name)
+
+    def asset_url(self, name: str) -> str:
+        assert name in self.asset_dict
+        return os.path.join(self.url_prefix, self.asset_dict[name], name)
+
+    def send_asset(self, name: str):
+        return send_from_directory(self.asset_folder, name)
diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index 13b174b6cc500574f399c2a37ddc45bda77feaf5..54024092f1288d253d4fc6b904def893879c835f 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -14,6 +14,7 @@ import werkzeug.formparser
 import mo
 import mo.config as config
 import mo.db as db
+import mo.ext.assets
 import mo.jobs
 import mo.rights
 import mo.users
@@ -107,6 +108,15 @@ def setup_logging():
 setup_logging()
 
 
+# Incializace assetového mechanismu
+mo.ext.assets.Assets(app, url_prefix='/assets', asset_dir=static_dir)
+
+app.assets.add_assets([
+    'bootstrap.min.css',
+    'mo.css',
+])
+
+
 # Inicializace požadavků a nucená autorizace
 
 class NeedLoginError(werkzeug.exceptions.Forbidden):
@@ -120,7 +130,7 @@ def need_login():
 
 def init_request():
     path = request.path
-    if path.startswith('/static/'):
+    if path.startswith('/static/') or path.startswith('/assets/'):
         # Pro statické soubory v development nasazení nepotřebujeme nastavovat
         # nic dalšího (v ostrém nasazení je servíruje uwsgi)
         return
diff --git a/mo/web/jinja.py b/mo/web/jinja.py
index 6b10303d4599e72bbcd3f278885a702f8b68cf23..860146355db7d45a503fd46826d947eab32a9366 100644
--- a/mo/web/jinja.py
+++ b/mo/web/jinja.py
@@ -1,4 +1,4 @@
-# Konfigurace Jinjový šablon a pomocné funkce
+# Konfigurace Jinjových šablon a pomocné funkce
 
 from flask import url_for
 from markupsafe import Markup
@@ -45,6 +45,7 @@ app.jinja_env.globals.update(Markup=Markup)
 
 app.jinja_env.globals.update(contest_breadcrumbs=contest_breadcrumbs)
 app.jinja_env.globals.update(place_breadcrumbs=place_breadcrumbs)
+# Funkce asset_url se přidává v mo.ext.assets
 
 
 @app.template_filter()
diff --git a/mo/web/templates/base.html b/mo/web/templates/base.html
index b87201488a68818bf73c55ee3e08391c563453b5..4413fc3dcce773096cbf69aaab6df90900822b1d 100644
--- a/mo/web/templates/base.html
+++ b/mo/web/templates/base.html
@@ -2,8 +2,8 @@
 <html>
 <head>
 	<title>Odevzdávací systém MO: {% block title %}{% endblock %}</title>
-	<link rel=stylesheet href="{{ url_for('static', filename='bootstrap.min.css') }}?v=2" type='text/css' media=all>
-	<link rel=stylesheet href="{{ url_for('static', filename='mo.css') }}?v=7" type='text/css' media=all>
+	<link rel=stylesheet href="{{ asset_url('bootstrap.min.css') }}" type='text/css' media=all>
+	<link rel=stylesheet href="{{ asset_url('mo.css') }}" type='text/css' media=all>
 {% block head %}{% endblock %}
 </head>
 <body>
diff --git a/setup.py b/setup.py
index 073ae8a17a5c03ae695caa6a9b5ee3d65e62a1b8..5b7705e6c8a4f95cd17c41da0ca88e673970ecb9 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ setuptools.setup(
     name='osmo',
     version='0.1',
     description='Odevzdávací systém Matematické olympiády',
-    packages=['mo', 'mo/jobs', 'mo/web'],
+    packages=['mo', 'mo/ext', 'mo/jobs', 'mo/web'],
     scripts=[
         'bin/add-role',
         'bin/create-contests',