diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..fc98bf2e1f0c916945f003419eb24ce875109320
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+.git
+.venv
+staticfiles/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cae401b3e48e85b933157a52c5b7bc42a0820683
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,30 @@
+stages:
+  - build
+
+image: docker:20.10.8
+
+variables:
+  DOCKER_TLS_CERTDIR: "/certs"
+  IMAGE_TAG_APP: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+  IMAGE_TAG_NGINX: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-nginx
+
+services:
+  - docker:20.10.8-dind
+
+before_script:
+  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+
+build_app:
+  stage: build
+  script:
+    - docker pull $CI_REGISTRY_IMAGE:test || true
+    - docker build --cache-from $CI_REGISTRY_IMAGE:test -t $IMAGE_TAG_APP .
+    - docker push $IMAGE_TAG_APP
+
+build_nginx:
+  stage: build
+  when: manual
+  script:
+    - docker pull $CI_REGISTRY_IMAGE:test-nginx || true
+    - docker build --cache-from $CI_REGISTRY_IMAGE:test-nginx -t $IMAGE_TAG_NGINX . -f Dockerfile.nginx
+    - docker push $IMAGE_TAG_NGINX
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..41366515096c6503c7e77927356a7c86270cca10
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+FROM python:3.10
+
+RUN mkdir /app
+WORKDIR /app
+
+RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash -
+RUN apt-get install nodejs && rm -rf /var/lib/apt/lists/*
+
+COPY . .
+
+RUN pip install -r requirements/base.txt -r requirements/prod.txt
+RUN npm install
+RUN npm run build
+RUN python manage.py collectstatic --noinput --settings=rybicka.settings.prod
+
+RUN bash -c "adduser --disabled-login --quiet --gecos app app &&  \
+             chmod -R o+r /app/ && \
+             chmod o+x /app/run.sh"
+USER app
+
+ENV DJANGO_SETTINGS_MODULE "rybicka.settings.prod"
+
+EXPOSE 8000
+
+CMD ["bash", "run.sh"]
diff --git a/Dockerfile.nginx b/Dockerfile.nginx
new file mode 100644
index 0000000000000000000000000000000000000000..47591546d366f11fba0362eff8a364dc8adf3c30
--- /dev/null
+++ b/Dockerfile.nginx
@@ -0,0 +1,3 @@
+FROM nginx:1.18
+EXPOSE 8080
+ADD nginx.conf /etc/nginx/conf.d/rybicka.conf
diff --git a/Makefile b/Makefile
index 0b53584209e8a36df2533979cf9dead37cd4184c..4dbde64bfe584eb70a83c33d63fe352f99d969a3 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 PYTHON   = python
 VENV     = .venv
 PORT     = 8012
-SETTINGS = rybicka.settings.dev
+SETTINGS = rybicka.settings.prod
 
 .PHONY: help venv install build run shell migrations migrate
 
@@ -26,7 +26,7 @@ venv: .venv/bin/python
 	${PYTHON} -m venv ${VENV}
 
 install: venv
-	${VENV}/bin/pip install -r requirements/base.txt
+	${VENV}/bin/pip install -r requirements/base.txt -r requirements/prod.txt
 	${VENV}/bin/nodeenv --python-virtualenv --node=19.3.0
 	${VENV}/bin/npm install
 
diff --git a/README.md b/README.md
index 68e8f3c76570663953abefe16e7d253ae1f04358..8e019db7722fccfbfdea993a54aba06d049a2ef5 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 Aplikace pro všemožné jednoduché nástroje, které pomohou automatizovat složité úkony.
 
-## Setup
+## Lokální setup
 
 Požadavky:
 - Python 3.9+
diff --git a/env.example b/env.example
index f1dd3ea0318034fdc38be0ca5c5f8a5dffc141da..5b8cb36653245921a245bda6313b083dc9a29e64 100644
--- a/env.example
+++ b/env.example
@@ -1,3 +1,6 @@
 DATABASE_URL="postgresql://rybicka:rybicka@localhost:5432/rybicka"
 
 SECRET_KEY="%@=^sip3=tqn6d_-xvvidc1@-t0t3&*kab@vr4c4"
+
+# Production settings
+ALLOWED_HOSTS="nastroje.pirati.cz"
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..16484fd52069292cf24727fbf8e393ac8583608e
--- /dev/null
+++ b/gunicorn.conf.py
@@ -0,0 +1,7 @@
+bind = "0.0.0.0:8000"
+accesslog = "-"
+workers = 1
+max_requests = 1000
+max_requests_jitter = 10
+timeout = 60
+graceful_timeout = 60
diff --git a/requirements/base.txt b/requirements/base.txt
index ca71420615d1b84383887baf24b85d7bf1780a85..150dea4b81c612a8cca7f11e79a8ebfaec5a9ce5 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,6 +1,6 @@
 Django==4.1.5
 django-database-url==1.0.3
-python-dotenv==0.21.0
+django-environ==0.9.0
 psycopg2-binary==2.9.5
 django-webpack-loader==1.8.0
 nodeenv==1.7.0
diff --git a/requirements/prod.txt b/requirements/prod.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9655c9a0a7bb5c95b0bd3a2f3d344abbb9a9269e
--- /dev/null
+++ b/requirements/prod.txt
@@ -0,0 +1,2 @@
+gunicorn==20.1.0 
+whitenoise==6.3.0
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5d9a7ada6bf4106d23769d523da59ae1e68e3720
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# exit on error
+set -e
+
+# migrate database
+python manage.py migrate
+
+# start webserver
+exec gunicorn -c gunicorn.conf.py rybicka.wsgi
diff --git a/rybicka/settings/base.py b/rybicka/settings/base.py
index 92b18225f8d28effdb5f8d8921b931e7c8bb071f..8eb3f074efd718e15cae089d7e2d56c583680aa8 100644
--- a/rybicka/settings/base.py
+++ b/rybicka/settings/base.py
@@ -14,27 +14,26 @@ import os
 import pathlib
 
 import dj_database_url
-import dotenv
-
-dotenv.load_dotenv()
+import environ
 
 # Build paths inside the project like this: BASE_DIR / "subdir".
 BASE_DIR = pathlib.Path(__file__).parents[2]
 
+
+env = environ.Env()
+environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
+
 # Quick-start development settings - unsuitable for production
 # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
 
 DEBUG = False
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = os.environ["SECRET_KEY"]
+SECRET_KEY = env.str("SECRET_KEY")
 
 ALLOWED_HOSTS = []
 
-STATIC_ROOT = os.environ.get(
-    "STATIC_ROOT",
-    "staticfiles"
-)
+STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
 
 # Application definition
 
diff --git a/rybicka/settings/prod.py b/rybicka/settings/prod.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3096fdebc72cd6a5c207ebce36484495a28d6c1
--- /dev/null
+++ b/rybicka/settings/prod.py
@@ -0,0 +1,12 @@
+"""
+Production settings.
+"""
+
+import os
+
+from .base import *
+
+ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
+
+MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
diff --git a/rybicka/wsgi.py b/rybicka/wsgi.py
index 8cfda5e82e09cf76fb8b577626815099008ca2d6..93e488cef367cc8af528c0a4737fcbcc1eb3c8d5 100644
--- a/rybicka/wsgi.py
+++ b/rybicka/wsgi.py
@@ -11,6 +11,6 @@ import os
 
 from django.core.wsgi import get_wsgi_application
 
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rybicka.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rybicka.settings.prod")
 
 application = get_wsgi_application()
diff --git a/member_group_size_calc/static/member_group_size_calc/calculator.webp b/shared/static/shared/calculator.webp
similarity index 100%
rename from member_group_size_calc/static/member_group_size_calc/calculator.webp
rename to shared/static/shared/calculator.webp
diff --git a/shared/templates/shared/index.html b/shared/templates/shared/index.html
index 4d196443a553c82eaaf0d8e7b9b29fb74a6f7bdf..ff6c42311ba3f11afc0ac261217204ff1b1e0222 100644
--- a/shared/templates/shared/index.html
+++ b/shared/templates/shared/index.html
@@ -18,7 +18,7 @@
            <article class="card">
                 <a href="{% url "member_group_size_calc_index" %}">
                     <img
-                        src="{% static "member_group_size_calc/calculator.webp" %}"
+                        src="{% static "shared/calculator.webp" %}"
                         alt="Kalkulačka výpočtu skupiny členů"
                         class="w-full h-48 object-cover"
                     >