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