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..1935004bf214c7b495dd6d1d327373c1ebaa64a4 --- /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 0000000000000000000000000000000000000000..9898bf91e48451044aafa04cab0d62ad11301f8e --- /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 db714730faca6eb3c2f22394d0768b2dc61ca394..4654125d2d215ad62b09cc7aa158ad1fe02823b3 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 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/nginx.conf b/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..08593c5c8d3c5bdc503d05b11f4fd8d8c9191527 --- /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 0000000000000000000000000000000000000000..9d41f264a6783a56a9c9360a5fdf46fa57a36f21 --- /dev/null +++ b/requirements-prod.txt @@ -0,0 +1 @@ +gunicorn==20.1.0 diff --git a/requirements.txt b/requirements.txt index e7eda36b0f3436231b6e8af5141a08cb3b2f13b5..d99efefa923ca134aba3fc1b64b9b579fafa1874 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 0000000000000000000000000000000000000000..652f2791a1907306a1c31feee54edc1d5290a71a --- /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