From a3f781e610dc0e83337447842dc3cce5f915ecdd Mon Sep 17 00:00:00 2001 From: Mawoka Date: Thu, 3 Mar 2022 17:02:01 +0100 Subject: [PATCH] :technologist: Added Docker --- Caddyfile-docker | 6 ++++ Dockerfile | 14 ++++++++++ docker-compose.yml | 65 +++++++++++++++++++++++++++++++++++++++++++ frontend/Dockerfile | 39 ++++++++++++++++++++++++++ gunicorn_conf.py | 67 +++++++++++++++++++++++++++++++++++++++++++++ prestart.sh | 1 + start.sh | 34 +++++++++++++++++++++++ 7 files changed, 226 insertions(+) create mode 100644 Caddyfile-docker create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 gunicorn_conf.py create mode 100644 prestart.sh create mode 100755 start.sh diff --git a/Caddyfile-docker b/Caddyfile-docker new file mode 100644 index 0000000..b5d3b10 --- /dev/null +++ b/Caddyfile-docker @@ -0,0 +1,6 @@ +:8080 { + reverse_proxy * http://frontend:3000 + reverse_proxy /api* http://api:80 + reverse_proxy /openapi.json http://api:80 # Only use if you need to serve the OpenAPI spec + reverse_proxy /docs http://api:80 # Only use if you need to serve the Swagger UI +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00c1ae6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10-slim +COPY classquiz/ /app/classquiz/ +COPY init_db.py /app/ +COPY *start.sh /app/ +COPY gunicorn_conf.py /app/ + +COPY Pipfile* /app/ +WORKDIR /app/ +RUN pip install pipenv && pipenv install --system +EXPOSE 80 +ENV PYTHONPATH=/app +RUN chmod +x start.sh +ENV APP_MODULE=classquiz:app +CMD ["./start.sh"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..68aad03 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +version: "3" + +services: + frontend: + restart: always + build: + context: ./frontend + dockerfile: Dockerfile + args: + redis: "" + depends_on: + - redis + - api + environment: + REDIS_URL: redis://redis:6379/0?decode_responses=True + API_URL: http://api:80 + + api: + build: + context: . + dockerfile: Dockerfile + restart: always + depends_on: + - db + - redis + + environment: + DB_URL: "postgresql://postgres:classquiz@db:5432/classquiz" + MAIL_ADDRESS: "email@email@email.email" + MAIL_PASSWORD: "PASSWORT" + MAIL_USERNAME: "email@email@email.email" + MAIL_SERVER: "email@email@email.emai" + MAX_WORKERS: "5" # Very important + MAIL_PORT: "587" + SECRET_KEY: "mysecretkey" + ACCESS_TOKEN_EXPIRE_MINUTES: 30 + SKIP_EMAIL_VERIFICATION: True + HCAPTCHA_KEY: "HCAPTCHA_PRIVATE_KEY" + + redis: + image: redis:alpine + restart: always + healthcheck: + test: [ "CMD", "redis-cli","ping" ] + + db: + image: postgres:alpine + restart: always + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 5s + timeout: 5s + retries: 5 + environment: + POSTGRES_PASSWORD: "classquiz" + POSTGRES_DB: "classquiz" + volumes: + - ./db:/var/lib/postgresql/data + proxy: + image: caddy:alpine + restart: unless-stopped + volumes: + - ./Caddyfile-docker:/etc/caddy/Caddyfile + ports: + - "8000:8080" \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..269e10e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,39 @@ +### Build Step +# pull the Node.js Docker image +ARG redis +FROM node:16 as builder +# ENV API_URL=http://api:80 +ENV API_URL=https://mawoka.eu +ENV REDIS_URL=$REDIS +# change working directory +WORKDIR /usr/src/app + +# copy the package.json files from local machine to the workdir in container +COPY package*.json ./ + +# run npm install in our local machine +RUN apt update && apt install -y curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && pnpm i + +# copy the generated modules and all other files to the container +COPY . . + +# build the application +RUN pnpm run build + +### Serve Step +# pull the Node.js Docker image +FROM node:16 + +# change working directory +WORKDIR /app +RUN apt update && apt install -y curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && apt purge curl -y && apt autoremove -y +# copy files from previous step +COPY --from=builder /usr/src/app/build . +COPY --from=builder /usr/src/app/package.json . +COPY --from=builder /usr/src/app/node_modules ./node_modules + +# our app is running on port 3000 within the container, so need to expose it +EXPOSE 3000 + +# the command that starts our app +CMD ["pnpm", "run", "run:prod"] \ No newline at end of file diff --git a/gunicorn_conf.py b/gunicorn_conf.py new file mode 100644 index 0000000..13b422a --- /dev/null +++ b/gunicorn_conf.py @@ -0,0 +1,67 @@ +import json +import multiprocessing +import os + +workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") +max_workers_str = os.getenv("MAX_WORKERS") +use_max_workers = None +if max_workers_str: + use_max_workers = int(max_workers_str) +web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) + +host = os.getenv("HOST", "0.0.0.0") +port = os.getenv("PORT", "80") +bind_env = os.getenv("BIND", None) +use_loglevel = os.getenv("LOG_LEVEL", "info") +if bind_env: + use_bind = bind_env +else: + use_bind = f"{host}:{port}" + +cores = multiprocessing.cpu_count() +workers_per_core = float(workers_per_core_str) +default_web_concurrency = workers_per_core * cores +if web_concurrency_str: + web_concurrency = int(web_concurrency_str) + assert web_concurrency > 0 +else: + web_concurrency = max(int(default_web_concurrency), 2) + if use_max_workers: + web_concurrency = min(web_concurrency, use_max_workers) +accesslog_var = os.getenv("ACCESS_LOG", "-") +use_accesslog = accesslog_var or None +proc_name = os.getenv("PROC_NAME", "classquiz_api") +errorlog_var = os.getenv("ERROR_LOG", "-") +use_errorlog = errorlog_var or None +graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") +timeout_str = os.getenv("TIMEOUT", "120") +keepalive_str = os.getenv("KEEP_ALIVE", "5") + +# Gunicorn config variables +loglevel = use_loglevel +workers = web_concurrency +bind = use_bind +errorlog = use_errorlog +worker_tmp_dir = "/dev/shm" +accesslog = use_accesslog +graceful_timeout = int(graceful_timeout_str) +timeout = int(timeout_str) +keepalive = int(keepalive_str) + +# For debugging and testing +log_data = { + "loglevel": loglevel, + "workers": workers, + "bind": bind, + "graceful_timeout": graceful_timeout, + "timeout": timeout, + "keepalive": keepalive, + "errorlog": errorlog, + "accesslog": accesslog, + # Additional, non-gunicorn variables + "workers_per_core": workers_per_core, + "use_max_workers": use_max_workers, + "host": host, + "port": port, +} +print(json.dumps(log_data)) diff --git a/prestart.sh b/prestart.sh new file mode 100644 index 0000000..41101f7 --- /dev/null +++ b/prestart.sh @@ -0,0 +1 @@ +python3 init_db.py diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..2514f3a --- /dev/null +++ b/start.sh @@ -0,0 +1,34 @@ +#! /usr/bin/env sh +set -e + +if [ -f /app/app/main.py ]; then + DEFAULT_MODULE_NAME=app.main +elif [ -f /app/main.py ]; then + DEFAULT_MODULE_NAME=main +fi +MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} +VARIABLE_NAME=${VARIABLE_NAME:-app} +export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} + +if [ -f /app/gunicorn_conf.py ]; then + DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py +elif [ -f /app/app/gunicorn_conf.py ]; then + DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py +else + DEFAULT_GUNICORN_CONF=/gunicorn_conf.py +fi +export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF} +export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"} + +# If there's a prestart.sh script in the /app directory or other path specified, run it before starting +PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh} +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . "$PRE_START_PATH" +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Gunicorn +exec gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE" \ No newline at end of file