From a9c59151fc7cb1d206275eff5c904dd88840b799 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Bedna=C5=99=C3=ADk?= <jan.bednarik@gmail.com>
Date: Wed, 8 Sep 2021 17:49:02 +0200
Subject: [PATCH] Deployment setup

---
 .gitlab-ci.yml          |  30 +++++++++
 Dockerfile              |  33 ++++++++++
 Dockerfile.nginx        |   3 +
 django_apps/settings.py | 142 ++++++++++++++++++++--------------------
 gunicorn.conf.py        |   7 ++
 nginx.conf              |  27 ++++++++
 requirements-prod.txt   |   1 +
 requirements.txt        |   3 +-
 run.sh                  |  10 +++
 9 files changed, 185 insertions(+), 71 deletions(-)
 create mode 100644 .gitlab-ci.yml
 create mode 100644 Dockerfile
 create mode 100644 Dockerfile.nginx
 create mode 100644 gunicorn.conf.py
 create mode 100644 nginx.conf
 create mode 100644 requirements-prod.txt
 create mode 100644 run.sh

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..cae401b
--- /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 0000000..1935004
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,33 @@
+FROM python:3.9
+
+RUN apt-get update && apt-get install -y \
+    build-essential python3-dev libdbus-glib-1-dev libgirepository1.0-dev libgirepository1.0-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN mkdir /app
+WORKDIR /app
+
+COPY requirements.txt .
+COPY requirements-prod.txt .
+RUN pip install -r requirements.txt -r requirements-prod.txt
+
+COPY . .
+
+RUN bash -c 'adduser --disabled-login --quiet --gecos app app &&  \
+             chmod -R o+r /app/ && \
+             mkdir /app/media && \
+             mkdir /app/static && \
+             chown -R app:app /app/media && \
+             chown -R app:app /app/static && \
+             chmod o+x /app/run.sh'
+USER app
+
+ENV DJANGO_SETTINGS_MODULE "django_apps.settings"
+
+# fake values for required env variables used to run collectstatic during build
+RUN SECRET_KEY=x DATABASE_URL=postgres://x/x DJANGO_ALLOWED_HOSTS=x \
+    python manage.py collectstatic
+
+EXPOSE 8000
+
+CMD ["bash", "run.sh"]
diff --git a/Dockerfile.nginx b/Dockerfile.nginx
new file mode 100644
index 0000000..9898bf9
--- /dev/null
+++ b/Dockerfile.nginx
@@ -0,0 +1,3 @@
+FROM nginx:1.21
+EXPOSE 8080
+ADD nginx.conf /etc/nginx/conf.d/sifrovacka.conf
diff --git a/django_apps/settings.py b/django_apps/settings.py
index db71473..4654125 100644
--- a/django_apps/settings.py
+++ b/django_apps/settings.py
@@ -11,90 +11,92 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
 """
 
 from pathlib import Path
-from os import getenv
+import environ
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
 BASE_DIR = Path(__file__).resolve().parent.parent
 
+env = environ.Env()
+environ.Env.read_env(str(BASE_DIR / ".env"))
 
 # Quick-start development settings - unsuitable for production
 # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = getenv('SECRET_KEY')
+SECRET_KEY = env.str("SECRET_KEY", default="")
 
 # SECURITY WARNING: don't run with debug turned on in production!
-#DEBUG = False
+# DEBUG = False
 
-DJANGO_ENV = getenv('DJANGO_ENV', 'production').lower()
-if DJANGO_ENV == 'local':
+DJANGO_ENV = env.str("DJANGO_ENV", default="production").lower()
+if DJANGO_ENV == "local":
     DEBUG = True
-elif DJANGO_ENV == 'production':
+elif DJANGO_ENV == "production":
     DEBUG = False
 
 
-ALLOWED_HOSTS = ['*']
+ALLOWED_HOSTS = ["*"]
 
 
 # Application definition
-    #'polls.apps.PollsConfig',
+#'polls.apps.PollsConfig',
 
 INSTALLED_APPS = [
-    'sifrovacka.apps.SifrovackaConfig',
-    'django_registration',
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'tinymce',
+    "sifrovacka.apps.SifrovackaConfig",
+    "django_registration",
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "tinymce",
 ]
 
 MIDDLEWARE = [
-    'django.middleware.security.SecurityMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'whitenoise.middleware.WhiteNoiseMiddleware',
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "whitenoise.middleware.WhiteNoiseMiddleware",
 ]
 
-ROOT_URLCONF = 'django_apps.urls'
+ROOT_URLCONF = "django_apps.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [],
-        'APP_DIRS': True,
-        'OPTIONS': {
-            'context_processors': [
-                'django.template.context_processors.debug',
-                'django.template.context_processors.request',
-                'django.template.context_processors.media',
-                'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.template.context_processors.media",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = 'django_apps.wsgi.application'
+WSGI_APPLICATION = "django_apps.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': BASE_DIR / 'db.sqlite3',
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": BASE_DIR / "db.sqlite3",
     }
 }
 
-if DJANGO_ENV == 'production':
+if DJANGO_ENV == "production":
     DATABASES = {"default": env.db("DATABASE_URL")}
 
 
@@ -103,16 +105,16 @@ if DJANGO_ENV == 'production':
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -120,9 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [
 # Internationalization
 # https://docs.djangoproject.com/en/3.2/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
 
 USE_I18N = True
 
@@ -134,58 +136,58 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/3.2/howto/static-files/
 
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
 
 # Default primary key field type
 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
 
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 
 # Custom settings AUTH
 
-LOGIN_REDIRECT_URL = 'home'
-LOGOUT_REDIRECT_URL = 'index'
+LOGIN_REDIRECT_URL = "home"
+LOGOUT_REDIRECT_URL = "index"
 
 # Custom settings STATIC
-STATIC_ROOT = BASE_DIR / 'static'
-#STATICFILES_DIRS = BASE_DIR / 'static'
+STATIC_ROOT = BASE_DIR / "static"
+# STATICFILES_DIRS = BASE_DIR / 'static'
 
 # Custom settings MEDIA
 MEDIA_URL = "/media/"
-MEDIA_ROOT = BASE_DIR / 'media'
+MEDIA_ROOT = BASE_DIR / "media"
 
 # TinyMCE settings
 TINYMCE_DEFAULT_CONFIG = {
-    'cleanup_on_startup': True,
-    'custom_undo_redo_levels': 20,
-    'selector': 'textarea',
-    'theme': 'silver',
-    'plugins': '''
+    "cleanup_on_startup": True,
+    "custom_undo_redo_levels": 20,
+    "selector": "textarea",
+    "theme": "silver",
+    "plugins": """
             textcolor save link image media preview codesample contextmenu
             table code lists fullscreen  insertdatetime  nonbreaking
             contextmenu directionality searchreplace wordcount visualblocks
             visualchars code fullscreen autolink lists  charmap print  hr
             anchor pagebreak
-            ''',
-    'toolbar1': '''
+            """,
+    "toolbar1": """
             fullscreen preview bold italic underline | fontselect,
             fontsizeselect  | forecolor backcolor | alignleft alignright |
             aligncenter alignjustify | indent outdent | bullist numlist table |
             | link image media | codesample |
-            ''',
-    'toolbar2': '''
+            """,
+    "toolbar2": """
             visualblocks visualchars |
             charmap hr pagebreak nonbreaking anchor |  code |
-            ''',
-    'contextmenu': 'formats | link image',
-    'menubar': True,
-    'statusbar': True,
+            """,
+    "contextmenu": "formats | link image",
+    "menubar": True,
+    "statusbar": True,
 }
 
 # Custom settings Registration
 ACCOUNT_ACTIVATION_DAYS = 7
-EMAIL_HOST = 'mail2.playzone.cz'
+EMAIL_HOST = "mail2.playzone.cz"
 EMAIL_PORT = 587
 EMAIL_USE_TLS = True
-EMAIL_HOST_USER = getenv('SMTP_USER')
-EMAIL_HOST_PASSWORD = getenv('SMTP_PASSWORD')
+EMAIL_HOST_USER = env.str("SMTP_USER", default="")
+EMAIL_HOST_PASSWORD = env.str("SMTP_PASSWORD", default="")
diff --git a/gunicorn.conf.py b/gunicorn.conf.py
new file mode 100644
index 0000000..16484fd
--- /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/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..08593c5
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,27 @@
+upstream sifrovacka {
+    ip_hash;
+    server sifrovacka_app:8000;
+}
+
+server {
+    server_name sifrovacka;
+    listen 8080;
+
+    client_max_body_size 10M;
+
+    proxy_connect_timeout   60;
+    proxy_send_timeout      60;
+    proxy_read_timeout      60;
+    send_timeout            60;
+
+    location /media/ {
+        alias /var/opt/sifrovacka/media/public/;
+    }
+
+    location / {
+        proxy_pass http://sifrovacka/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-Proto https;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+}
diff --git a/requirements-prod.txt b/requirements-prod.txt
new file mode 100644
index 0000000..9d41f26
--- /dev/null
+++ b/requirements-prod.txt
@@ -0,0 +1 @@
+gunicorn==20.1.0
diff --git a/requirements.txt b/requirements.txt
index e7eda36..d99efef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,6 +11,7 @@ dbus-python==1.2.16
 decorator==5.0.9
 distlib==0.3.1
 Django==3.2.3
+django-environ==0.6.0
 django-registration==3.2
 django-tinymce==3.3.0
 entrypoints==0.3
@@ -33,6 +34,7 @@ pexpect==4.8.0
 pickleshare==0.7.5
 Pillow==8.3.2
 prompt-toolkit==3.0.20
+psycopg2-binary==2.9.1
 ptyprocess==0.7.0
 pycairo==1.20.1
 pycparser==2.20
@@ -50,7 +52,6 @@ sqlparse==0.4.1
 traitlets==5.0.5
 typing-extensions==3.10.0.0
 urllib3==1.26.6
-virtualenv==20.4.0+ds
 wcwidth==0.2.5
 whitenoise==5.3.0
 xdg==5
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000..652f279
--- /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 django_apps.wsgi
-- 
GitLab