From eace686e59a27a0df5ad14230a6009243357f97b Mon Sep 17 00:00:00 2001 From: Topvennie Date: Mon, 11 Mar 2024 21:19:11 +0100 Subject: [PATCH] feat(deployement): production environment --- .dev.env | 20 +++++ .env | 13 --- .gitignore | 1 + .prod.env | 30 +++++++ backend/{Dockerfile => Dockerfile.dev} | 0 backend/Dockerfile.prod | 15 ++++ backend/gunicorn_config.py | 4 + backend/requirements.txt | 3 +- backend/ypovoli/settings.py | 33 ++++--- backend/ypovoli/urls.py | 43 +++++---- data/nginx/nginx.conf | 52 ----------- development.sh | 5 ++ development.yml | 12 +-- frontend/{Dockerfile => Dockerfile.dev} | 0 frontend/Dockerfile.prod | 12 +++ frontend/nginx.conf | 9 ++ production.yml | 110 ++++++++++++++++++++++++ 17 files changed, 258 insertions(+), 104 deletions(-) create mode 100644 .dev.env delete mode 100644 .env create mode 100644 .prod.env rename backend/{Dockerfile => Dockerfile.dev} (100%) create mode 100644 backend/Dockerfile.prod create mode 100644 backend/gunicorn_config.py delete mode 100644 data/nginx/nginx.conf rename frontend/{Dockerfile => Dockerfile.dev} (100%) create mode 100644 frontend/Dockerfile.prod create mode 100644 frontend/nginx.conf create mode 100644 production.yml diff --git a/.dev.env b/.dev.env new file mode 100644 index 00000000..890efd41 --- /dev/null +++ b/.dev.env @@ -0,0 +1,20 @@ +PUID=1000 +PGID=1000 +TZ=Europe/Brussels + +DATADIR=./data + +BACKEND_DIR=./backend + +FRONTEND_DIR=./frontend + +REDIS_IP=192.168.90.10 +REDIS_PORT=6379 + +DJANGO_SECRET_KEY= +DJANGO_DEBUG=True +DJANGO_DB_ENGINE=django.db.backends.sqlite3 +DJANGO_DB_NAME=${BACKEND_DIR}/db.sqlite3 +DJANGO_REDIS_HOST=${REDIS_IP} +DJANGO_REDIS_PORT=${REDIS_PORT} +DJANGO_REDIS_PASSWORD=${REDIS_PASSWORD} \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index 0d3f97be..00000000 --- a/.env +++ /dev/null @@ -1,13 +0,0 @@ -PUID=1000 -PGID=1000 -TZ="Europe/Brussels" - -DATADIR="./data" - -BACKEND_DIR="./backend" - -FRONTEND_DIR="./frontend" - -REDIS_IP="192.168.90.10" -REDIS_PORT=6379 -REDIS_PASSWORD="oqOsNX1PXGOX5soJtKkw" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5dafea5e..6a9bedb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .tool-versions +.env data/* !data/nginx/nginx.conf diff --git a/.prod.env b/.prod.env new file mode 100644 index 00000000..7cf0fc6e --- /dev/null +++ b/.prod.env @@ -0,0 +1,30 @@ +PUID=1000 +PGID=1000 +TZ=Europe/Brussels + +DATADIR=./data + +BACKEND_DIR=./backend + +FRONTEND_DIR=./frontend + +POSTGRES_IP=192.168.90.9 +POSTGRES_PORT=5432 +POSTGRES_DB=selab +POSTGRES_USER=selab_user +POSTGRES_PASSWORD= + +REDIS_IP=192.168.90.10 +REDIS_PORT=6379 + +DJANGO_SECRET_KEY= +DJANGO_DEBUG=false +DJANGO_DB_ENGINE=django.db.backends.postgresql +DJANGO_DB_NAME=${POSTGRES_DB} +DJANGO_DB_USER=${POSTGRES_USER} +DJANGO_DB_PASSWORD=${POSTGRES_PASSWORD} +DJANGO_DB_HOST=${POSTGRES_IP} +DJANGO_DB_PORT=${POSTGRES_PORT} +DJANGO_REDIS_HOST=${REDIS_IP} +DJANGO_REDIS_PORT=${REDIS_PORT} +DJANGO_REDIS_PASSWORD=${REDIS_PASSWORD} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile.dev similarity index 100% rename from backend/Dockerfile rename to backend/Dockerfile.dev diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod new file mode 100644 index 00000000..1377e32d --- /dev/null +++ b/backend/Dockerfile.prod @@ -0,0 +1,15 @@ +FROM python:3.11.4 + +RUN apt update && apt install -y gettext libgettextpo-dev && pip install --upgrade pip + +WORKDIR /code + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN ./setup.sh diff --git a/backend/gunicorn_config.py b/backend/gunicorn_config.py new file mode 100644 index 00000000..6ec33776 --- /dev/null +++ b/backend/gunicorn_config.py @@ -0,0 +1,4 @@ +workers = 4 +bind = "0.0.0.0:8080" +chdir = "/code/" +module = "ypovoli.wsgi:application" diff --git a/backend/requirements.txt b/backend/requirements.txt index d1dab654..addd2c6c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,4 +8,5 @@ cas-client==1.0.0 psycopg2-binary==2.9.9 djangorestframework-simplejwt==5.3.1 celery[redis]==5.3.6 -django-redis==5.4.0 \ No newline at end of file +django-redis==5.4.0 +gunicorn==21.2.0 \ No newline at end of file diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 7f1a3107..7adda2b5 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -10,10 +10,10 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ """ +import os from datetime import timedelta from os import environ from pathlib import Path -import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -26,10 +26,10 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-_upw+)mo--8_0slsl&8ot0*h8p50z_rlid6nwobd*%%gm$_!1x" +SECRET_KEY = environ.get("DJANGO_SECRET_KEY", "lnZZ2xHc6HjU5D85GDE3Nnu4CJsBnm") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = environ.get("DJANGO_DEBUG", False) ALLOWED_HOSTS = [] @@ -89,17 +89,20 @@ # Application endpoints CAS_ENDPOINT = "https://login.ugent.be" -CAS_RESPONSE = "https://localhost:8080/auth/cas/echo" -API_ENDPOINT = "https://localhost:8080" +CAS_RESPONSE = "https://localhost:8080/api/auth/cas/echo" +API_ENDPOINT = "https://localhost:8080/api" # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": environ.get("DJANGO_DB_ENGINE", "django.db.backends.sqlite3"), + "NAME": environ.get("DJANGO_DB_NAME", BASE_DIR / "db.sqlite3"), + "USER": environ.get("DJANGO_DB_USER", ""), + "PASSWORD": environ.get("DJANGO_DB_PASSWORD", ""), + "HOST": environ.get("DJANGO_DB_HOST", ""), + "PORT": environ.get("DJANGO_DB_PORT", ""), }, - "production": {"ENGINE": "django.db.backends.postgresql"}, } # Default primary key field type @@ -145,9 +148,8 @@ } REDIS_CUSTOM = { - "host": environ.get("REDIS_IP", "localhost"), - "port": environ.get("REDIS_PORT", 6379), - "password": environ.get("REDIS_PASSWORD", ""), + "host": environ.get("DJANGO_REDIS_HOST", "localhost"), + "port": environ.get("DJANGO_REDIS_PORT", 6379), "db_django": 0, "db_celery": 1, } @@ -155,15 +157,12 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": f"redis://:{REDIS_CUSTOM['password']}@{REDIS_CUSTOM['host']}:{REDIS_CUSTOM['port']}/" - f"{REDIS_CUSTOM['db_django']}", + "LOCATION": f"redis://@{REDIS_CUSTOM['host']}:{REDIS_CUSTOM['port']}/{REDIS_CUSTOM['db_django']}", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, } } -CELERY_BROKER_URL = ( - f"redis://:{REDIS_CUSTOM['password']}@{REDIS_CUSTOM['host']}:{REDIS_CUSTOM['port']}/" - f"{REDIS_CUSTOM['db_celery']}" -) +CELERY_BROKER_URL = f"redis://@{REDIS_CUSTOM['host']}:{REDIS_CUSTOM['port']}/{REDIS_CUSTOM['db_celery']}" +CELERY_RESULT_BACKEND = f"redis://@{REDIS_CUSTOM['host']}:{REDIS_CUSTOM['port']}/{REDIS_CUSTOM['db_celery']}" diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index f3093cdc..fdcdc726 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -15,7 +15,7 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.urls import include, path +from django.urls import include, path, re_path from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework import permissions @@ -33,18 +33,31 @@ urlpatterns = [ - # Base API endpoints. - path("", include("api.urls")), - # Authentication endpoints. - path("auth/", include("authentication.urls")), - path("notifications/", include("notifications.urls"), name="notifications"), - # Swagger documentation. - path( - "swagger/", - schema_view.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - path( - "swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json" - ), + re_path( + "api/", + include( + [ + # Base API endpoints. + path("", include("api.urls")), + # Authentication endpoints. + path("auth/", include("authentication.urls")), + path( + "notifications/", + include("notifications.urls"), + name="notifications", + ), + # Swagger documentation. + path( + "swagger/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path( + "swagger/", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + ] + ), + ) ] diff --git a/data/nginx/nginx.conf b/data/nginx/nginx.conf deleted file mode 100644 index f6a71bee..00000000 --- a/data/nginx/nginx.conf +++ /dev/null @@ -1,52 +0,0 @@ -events { - worker_connections 1024; -} - -http { - upstream backend { - server backend:8080; - } - - upstream frontend { - server frontend:5173; - } - - server { - listen 80; - listen [::]:80; - - location / { - return 301 https://$host$request_uri; - } - } - - server { - listen 443 ssl; - listen [::]:443 ssl; - - ssl_certificate ssl/certificate.crt; - ssl_certificate_key ssl/private.key; - - location / { - proxy_pass http://frontend; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - proxy_redirect off; - } - } - - server { - listen 8080 ssl; - listen [::]:8080 ssl; - - ssl_certificate ssl/certificate.crt; - ssl_certificate_key ssl/private.key; - - location / { - proxy_pass https://backend; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - proxy_redirect off; - } - } -} diff --git a/development.sh b/development.sh index 24697323..210027f6 100755 --- a/development.sh +++ b/development.sh @@ -1,3 +1,8 @@ +if ! [ -f .env ]; then + echo "Error: .env file does not exist." + exit 1 +fi + echo "Checking for existing SSL certificates..." if [ ! -f "data/nginx/ssl/private.key" ] || [ ! -f "data/nginx/ssl/certificate.crt" ]; then diff --git a/development.yml b/development.yml index 2530320b..6aeb479d 100644 --- a/development.yml +++ b/development.yml @@ -38,7 +38,7 @@ services: - 443:443 - 8080:8080 volumes: - - $DATADIR/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - $DATADIR/nginx/nginx.dev.conf:/etc/nginx/nginx.conf:ro - $DATADIR/nginx/ssl:/etc/nginx/ssl:ro depends_on: - backend @@ -49,10 +49,10 @@ services: container_name: backend build: context: $BACKEND_DIR - dockerfile: Dockerfile + dockerfile: Dockerfile.dev command: bash -c "./setup.sh && python manage.py runsslserver 192.168.90.2:8080" expose: - - 8000 + - 8080 volumes: - $BACKEND_DIR:/code @@ -61,8 +61,8 @@ services: container_name: celery build: context: $BACKEND_DIR - dockerfile: Dockerfile - command: celery -A ypovoli worker -l INFO + dockerfile: Dockerfile.dev + command: celery -A ypovoli worker -l DEBUG volumes: - $BACKEND_DIR:/code depends_on: @@ -74,7 +74,7 @@ services: container_name: frontend build: context: $FRONTEND_DIR - dockerfile: Dockerfile + dockerfile: Dockerfile.dev command: bash -c "npm install && npm run host" expose: - 5173 diff --git a/frontend/Dockerfile b/frontend/Dockerfile.dev similarity index 100% rename from frontend/Dockerfile rename to frontend/Dockerfile.dev diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 00000000..5254aa37 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,12 @@ +FROM node:16 as build-stage +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY ./ . +RUN npm run build + +FROM nginx as production-stage +EXPOSE 3000 +RUN mkdir /app +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build-stage /app/dist /app \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..0ae8c83e --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 3000; + + location / { + root /app; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/production.yml b/production.yml new file mode 100644 index 00000000..4c416e8b --- /dev/null +++ b/production.yml @@ -0,0 +1,110 @@ +version: "3.9" + +############################# NETWORKS + +networks: + selab_network: + name: selab_network + driver: bridge + ipam: + config: + - subnet: 192.168.90.0/24 + +############################# EXTENSIONS + +x-common-keys-selab: &common-keys-selab + networks: + - selab_network + security_opt: + - no-new-privileges:true + restart: unless-stopped + environment: + TZ: $TZ + PUID: $PUID + PGID: $PGID + env_file: + - .env + +############################# SERVICES + +services: + + nginx: + <<: *common-keys-selab + image: nginx:latest + container_name: nginx + ports: + - 80:80 + - 443:443 + - 8080:8080 + volumes: + - $DATADIR/nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro + - $DATADIR/nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - backend + - frontend + + postgres: + <<: *common-keys-selab + image: postgres:15.2 + container_name: postgres + networks: + selab_network: + ipv4_address: $POSTGRES_IP + environment: + POSTGRES_DB: $POSTGRES_DB + POSTGRES_USER: $POSTGRES_USER + POSTGRES_PASSWORD: $POSTGRES_PASSWORD + expose: + - $POSTGRES_PORT + volumes: + - $DATADIR/postgres:/var/lib/postgresql/data + + backend: + <<: *common-keys-selab + container_name: backend + build: + context: $BACKEND_DIR + dockerfile: Dockerfile.prod + command: bash -c "./setup.sh && gunicorn --config gunicorn_config.py ypovoli.wsgi:application" + expose: + - 8080 + depends_on: + - postgres + + redis: + <<: *common-keys-selab + container_name: redis + image: redis:latest + networks: + selab_network: + ipv4_address: $REDIS_IP + expose: + - $REDIS_PORT + entrypoint: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru + volumes: + - $DATADIR/redis:/data + + celery: + <<: *common-keys-selab + container_name: celery + build: + context: $BACKEND_DIR + dockerfile: Dockerfile.prod + command: celery -A ypovoli worker -l ERROR + volumes: + - $BACKEND_DIR:/code + depends_on: + - backend + - redis + + frontend: + <<: *common-keys-selab + container_name: frontend + build: + context: $FRONTEND_DIR + dockerfile: Dockerfile.prod + expose: + - 3000 + depends_on: + - backend \ No newline at end of file