diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..d859323e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,71 @@ +name: Build + +on: + pull_request: + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.github/**' + - '!.github/workflows/build.yml' + push: + branches: + - main + tags: '[0-9]+.[0-9]+.[0-9]+' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.github/**' + - '!.github/workflows/build.yml' + release: + types: + - created + +jobs: + docker: + name: Build and Push Docker Image + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'ls1intum/Pyris' }} + runs-on: ubuntu-latest + steps: + - name: Compute Tag + uses: actions/github-script@v6 + id: compute-tag + with: + result-encoding: string + script: | + if (context.eventName === "pull_request") { + return "pr-" + context.issue.number; + } + if (context.eventName === "release") { + return "latest"; + } + if (context.eventName === "push") { + if (context.ref.startsWith("refs/tags/")) { + return context.ref.slice(10); + } + if (context.ref === "refs/heads/main") { + return "latest"; + } + } + return "FALSE"; + - uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + # Build and Push to GitHub Container Registry + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push to GitHub Container Registry + uses: docker/build-push-action@v4 + if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} + with: + platforms: amd64, arm64 + file: ./Dockerfile + context: . + tags: ghcr.io/ls1intum/pyris:${{ steps.compute-tag.outputs.result }} + push: true diff --git a/.gitignore b/.gitignore index 2dc53ca3..06f43f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +####################### +# Custom rules # +####################### +application.local.yml +llm_config.local.yml + + +######################## +# Auto-generated rules # +######################## # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f9c1b986 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Dockerfile to build a container image for a Python 3.12 FastAPI application +FROM python:3.12-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the dependencies file to the working directory +COPY requirements.txt . + +# Install any dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the content of the local src directory to the working directory +COPY app/ ./app + +# Specify the command to run on container start +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker/nginx.yml b/docker/nginx.yml new file mode 100644 index 00000000..f6aeaa6b --- /dev/null +++ b/docker/nginx.yml @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Nginx base service +# ---------------------------------------------------------------------------------------------------------------------- + +services: + nginx: + container_name: pyris-nginx + image: nginx:1.23 + pull_policy: always + volumes: + - ./nginx/timeouts.conf:/etc/nginx/conf.d/timeouts.conf:ro + - ./nginx/pyris-nginx.conf:/etc/nginx/conf.d/pyris-nginx.conf:ro + - ./nginx/pyris-server.conf:/etc/nginx/includes/pyris-server.conf:ro + - ./nginx/dhparam.pem:/etc/nginx/dhparam.pem:ro + - ./nginx/nginx_502.html:/usr/share/nginx/html/502.html:ro + - ./nginx/70-pyris-setup.sh:/docker-entrypoint.d/70-pyris-setup.sh + - ./nginx/certs/pyris-nginx+4.pem:/certs/fullchain.pem:ro + - ./nginx/certs/pyris-nginx+4-key.pem:/certs/priv_key.pem:ro + ports: + - "80:80" + - "443:443" + # expose the port to make it reachable docker internally even if the external port mapping changes + expose: + - "80" + - "443" + healthcheck: + test: service nginx status || exit 1 + start_period: 60s + networks: + - pyris + +networks: + pyris: + driver: "bridge" + name: pyris \ No newline at end of file diff --git a/docker/nginx/70-pyris-setup.sh b/docker/nginx/70-pyris-setup.sh new file mode 100755 index 00000000..37a73516 --- /dev/null +++ b/docker/nginx/70-pyris-setup.sh @@ -0,0 +1,2 @@ +# disable default.conf +mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.disabled || true diff --git a/docker/nginx/certs/pyris-nginx+4-key.pem b/docker/nginx/certs/pyris-nginx+4-key.pem new file mode 100644 index 00000000..eb2d95da --- /dev/null +++ b/docker/nginx/certs/pyris-nginx+4-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCuCJSHStSQd02f +j+IlQFes7pVUcYv2r0qm5qicGwPcKQf1/nmsy6k4WhE9HQV9VO9LQ4doSNp9NuYX +P/JQqdYLZYYQvxHS+fR7ofIPjirsrbQYAkG5F6imM8H7MkkueG3HGqaKD54PBmC4 +BgBJDFWiF8jSNSYNKOE2L5SaYG/g3LLIkWBlhBQHgrprkio4pv5Y44+nf+hGWkSj +bkRo2+PmIsNmQrpDB2o0O7uoyswa71HE967n9K17SWZ7Hi4kP6BGUWn65P5JB10a +6kz0y8183Uzz99bx8hzxLPg6VNiJZQ+dH4M1Jn6kysKiyV4x24JsM9s6t+Vhln9E +KX5ktosdAgMBAAECggEBAJs3ddkwqWLrtOSR/H2C5G+NHsyAtPdgIfG3mTwZcBjk +03/X5gdyYUusMOHTx3ifzwjOgq9FAvFYjGDCHMlKoGfrtWWsNCZ53k6CApVTE/+h +cRVUte9yJW2Ojf0PPWvf5vEEWPKbuTnnU03ttEVyZdG66tZoprZn9m1QhHYnesEO +PMPvYMd3Oyko8MD/Rr1A/KS/rmc0yfUvgLsqF6PLxq3NKxyVD/8Tp4u9aXbPMnd2 +vugVxjjvt5ubscF1Owi8EjqjVkXlw94JzLcy70XfBzsS2EvUtX/hmHgBEsViXUOQ +KGVyeFTvuReq0RvLQi1LA8vs2q6UC0ZYX75wGDfWWnUCgYEAyP6FY6xdP+N83qEM +TzAf2a33bBCcD5zbrfsvYwHwdzcAz9HBdf3TN1ZcbgfIzIWvuo+hFdjZd32E2+b7 +tSGpcs21iZ3dn1aWxngNs/h94h6cNak/02iCbOsmMX9rHfKZd1ODnQyA8q0s9PQY +uWWWMUfqPse7mSYbgU0aYOVFraMCgYEA3ak3N2mTgTVsUqhNyZCJlmtafp0tsT6b +/7GKSqkl741wokM6un3wx1eo6Q95mngxOlY2xxq9OChnNSEa9ZQnzdUDtQ0YE4QD +09awTIMHNCeSqpV2n3Yv2fT3C5Ya5/WEtYGpVAtqgxwWPij8+VMOa8MVzy+/v6Hg +N1Tpww+Y8D8CgYEAhbEGeK4FuKFQRaVJ0sJn7RrSIIdLxvbHCIqzkl+P2zwyxgj3 +bcxP2dcP1ABJiADESouO0kFTJS/QV5TkiC7DzyEVR1xCNeIamBjyxGrdELLbpLXX +Rn+VgW1IElR2o4zil4RtXuEaRFD8PlK+v1La/ByhqvCfz9aRJQhsK1dVaZECgYEA +jRYR0TFf89P/OLVrnapkCNwX45ND7Bc/0AY/UbpMLSfH02AbV2yl/xvqpT12Vz29 +h7Ysc5qvabk9x/FkaX99vmOhUnIdKv7SONnjqS+VPDsb/XvY3zKozoA/Zp6KTa5W +Y/k9wALsLruH5NTOABw/h5PKo+9uixkLz+w6Ri/9Vp0CgYEAqfkZJe7vCOIwtIwj +Mq5knkJgR+Vq30i4jRoFU0yxIcWA1hODVBnK39+mtA++/3+r5DY5fGRTc9mMyXU/ +y2N2nfSnvPMAUaRmisB7NhmvinEgymlrX+WE+7S9/+nOQADxzWSc6Hxg/ub6mTYV +k2/hv9uG1gbm2+OBP/EBOr48jz0= +-----END PRIVATE KEY----- diff --git a/docker/nginx/certs/pyris-nginx+4.pem b/docker/nginx/certs/pyris-nginx+4.pem new file mode 100644 index 00000000..64927868 --- /dev/null +++ b/docker/nginx/certs/pyris-nginx+4.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIERjCCAq6gAwIBAgIQSQ2vfdquHAQcrzbEKx46mzANBgkqhkiG9w0BAQsFADBf +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExGjAYBgNVBAsMEXJvb3RA +MWY0ZmQzNzYzNmMyMSEwHwYDVQQDDBhta2NlcnQgcm9vdEAxZjRmZDM3NjM2YzIw +HhcNMjIxMjA1MDk0NTEzWhcNMjUwMzA1MDk0NTEzWjBFMScwJQYDVQQKEx5ta2Nl +cnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxGjAYBgNVBAsMEXJvb3RAMWY0ZmQz +NzYzNmMyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArgiUh0rUkHdN +n4/iJUBXrO6VVHGL9q9KpuaonBsD3CkH9f55rMupOFoRPR0FfVTvS0OHaEjafTbm +Fz/yUKnWC2WGEL8R0vn0e6HyD44q7K20GAJBuReopjPB+zJJLnhtxxqmig+eDwZg +uAYASQxVohfI0jUmDSjhNi+UmmBv4NyyyJFgZYQUB4K6a5IqOKb+WOOPp3/oRlpE +o25EaNvj5iLDZkK6QwdqNDu7qMrMGu9RxPeu5/Ste0lmex4uJD+gRlFp+uT+SQdd +GupM9MvNfN1M8/fW8fIc8Sz4OlTYiWUPnR+DNSZ+pMrCosleMduCbDPbOrflYZZ/ +RCl+ZLaLHQIDAQABo4GXMIGUMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr +BgEFBQcDATAfBgNVHSMEGDAWgBSpuKALkiwfLnQmm7+JNG2bxGAIgzBMBgNVHREE +RTBDgg1hcnRlbWlzLW5naW54gg9hcnRlbWlzLmV4YW1wbGWCCWxvY2FsaG9zdIcE +fwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAYEApG8ZADQe +SsH/nqH9WpR3ZkYg0rm8pw+YquBNUdDFG2/4IQtaaxrgsvNPrEEMXfCO4vvnC0cH +6Tgay8LzFZxU9D1F06VZ9S1C7KNnYSsjgwhW7wxem1JXgauoutA8D0uHLr/2bVnz +rTShQT7gRp9SRunqDylaSkgpXlfZQRlEANrYT8Jh6LIHRjkxLh/etw7VdFA6Tywh +iQGBE/EbQcGpmqHBoMytblku0D8H+pcFHZ03AZq0FTMbByM9GekQ8HJV88epqvqJ +7pWyQPX9lr7yC6n121dPoA0ylP8D7jIBCmlFeF+QWCiRAgdeb1w+JONHMgI97IR+ +9HBm6gGE+Da/TRq82w02tUN/F7NHdzqwKGx/GKLrEsdNlfP6D9iiVtfBGBoAUm+C +2t3jbQEgqYHA+mzadS75RGJsRnVdY24IHvNjEnESW6KCaSfQyMmp3trRH6JeOttU +2JeqRPjmOzNvzIcB76w1/hB2ljhimyfoxB8Gbrts+GFPRZE+AXg1mvCn +-----END CERTIFICATE----- diff --git a/docker/nginx/dhparam.pem b/docker/nginx/dhparam.pem new file mode 100644 index 00000000..445feef7 --- /dev/null +++ b/docker/nginx/dhparam.pem @@ -0,0 +1,13 @@ +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEAiT9FabLCTYkbfSNvenLC4q5kWMNIjCUTcYWZpt8xOlmrJQqgui9h +lKXP3hDe0J50oqYUkDQ+YS8i+GCVLzAJqXixqynLqrz/v5IWgloQMJJKlPBEl9M6 +/Kh40+VyasVz7toja4qbyN12Kz1S8qLOlmxCcPmOpxGwIUG2yYSuZH9JQ724Gnji +4puFw3CXDm6aBZWBb7cpwEvHSVW5C9R+Acph7ahCby9kWpLrLsNHL73+jJiJSGcx +hig+Yie2XTTlUBHVcxlHCZu8pFXA40hLuagejmXGuVfaaoezMyU1OpfrJpsJSE2s +OxFEt01nCaEguNn7L1dr46fHWux651/UCRHR8MB0J6KEOuKDhgQ8bq+WSGlowaJM +NGhGxAlFH98D/gbOrVcRxJDpmaSFVVwO4piDT/pBDvzaS6Ll8dnoKLv8TNa2r7dG +gedlnJ2gIhU3lLLjqIwe+fmrfhlr3ybwuIiSx/efEaw65vDnOkOHeKKXtbxUAMS6 +07bLIKLEw4QRwMmrLhzu2sZnFipAppXjsQ8tRa/QO4eoaEM97FKq6qONVwAA2if6 +l3amSySYVDvMYpaOwQYawKTole1Kon06h8JlIr+A5W3vmraMfQZZY72HAkxuOYH0 +wchOEYKU+jlmutbEdz747Ngleb5kp55CtL/PlEawEpqXWWXYBqo8mmMCAQI= +-----END DH PARAMETERS----- diff --git a/docker/nginx/nginx_502.html b/docker/nginx/nginx_502.html new file mode 100644 index 00000000..59cb0f29 --- /dev/null +++ b/docker/nginx/nginx_502.html @@ -0,0 +1,27 @@ + + + +Pyris Maintenance + + + + +
+ Asset 3 +

We’ll be back soon!

+
+

We’re performing some maintenance at the moment. Sorry for the inconvenience.

+

— Your Pyris Administrators

+
+
+ diff --git a/docker/nginx/pyris-nginx.conf b/docker/nginx/pyris-nginx.conf new file mode 100644 index 00000000..4b9372ac --- /dev/null +++ b/docker/nginx/pyris-nginx.conf @@ -0,0 +1,41 @@ +# Load balancing +upstream pyris { + server pyris-app:8000; +} + +# Remove nginx version from HTTP response +server_tokens off; + +# Rate limit for the login REST call, at most one requests per two seconds +limit_req_zone $binary_remote_addr zone=loginlimit:10m rate=30r/m; + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name _; + + ssl_certificate /certs/fullchain.pem; + ssl_certificate_key /certs/priv_key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + # TODO: dynamic dh param generation not needed here? Otherwise have to generate them somehow if not available at container entrypoint + ssl_dhparam /etc/nginx/dhparam.pem; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; + ssl_ecdh_curve secp384r1; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + ssl_stapling on; + ssl_stapling_verify on; +# ssl_early_data on; + + include includes/pyris-server.conf; +} diff --git a/docker/nginx/pyris-server.conf b/docker/nginx/pyris-server.conf new file mode 100644 index 00000000..26171a10 --- /dev/null +++ b/docker/nginx/pyris-server.conf @@ -0,0 +1,32 @@ +resolver 127.0.0.11; +resolver_timeout 5s; +client_max_body_size 10m; +client_body_buffer_size 1m; + +location / { + proxy_pass http://pyris; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; +# proxy_set_header Early-Data $ssl_early_data; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_send_timeout 900s; + proxy_read_timeout 900s; + proxy_max_temp_file_size 0; + proxy_buffering on; + proxy_buffer_size 16k; + proxy_buffers 8 16k; + proxy_busy_buffers_size 32k; + fastcgi_send_timeout 900s; + fastcgi_read_timeout 900s; + client_max_body_size 128M; + } + +error_page 502 /502.html; +location /502.html { + root /usr/share/nginx/html; + internal; +} diff --git a/docker/nginx/timeouts.conf b/docker/nginx/timeouts.conf new file mode 100644 index 00000000..1142cf3a --- /dev/null +++ b/docker/nginx/timeouts.conf @@ -0,0 +1,4 @@ +proxy_send_timeout 900s; +proxy_read_timeout 900s; +fastcgi_send_timeout 900s; +fastcgi_read_timeout 900s; diff --git a/docker/pyris-dev.yml b/docker/pyris-dev.yml new file mode 100644 index 00000000..0d67e3ee --- /dev/null +++ b/docker/pyris-dev.yml @@ -0,0 +1,21 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Setup for a Pyris development server. +# ---------------------------------------------------------------------------------------------------------------------- + +services: + pyris-app: + extends: + file: ./pyris.yml + service: pyris-app + pull_policy: never + restart: "no" + volumes: + - ../application.local.yml:/config/application.yml:ro + - ../llm_config.local.yml:/config/llm_config.yml:ro + networks: + - pyris + +networks: + pyris: + driver: "bridge" + name: pyris \ No newline at end of file diff --git a/docker/pyris-production.yml b/docker/pyris-production.yml new file mode 100644 index 00000000..ebe9ef3f --- /dev/null +++ b/docker/pyris-production.yml @@ -0,0 +1,42 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Setup for a Pyris production server. +# ---------------------------------------------------------------------------------------------------------------------- +# It is designed to take in a lot of environment variables to take in all the configuration of the deployment. +# ---------------------------------------------------------------------------------------------------------------------- + +services: + pyris-app: + extends: + file: ./pyris.yml + service: pyris-app + image: ghcr.io/ls1intum/pyris:${PYRIS_DOCKER_TAG:-latest} + depends_on: + nginx: + condition: service_started + pull_policy: always + restart: unless-stopped + volumes: + - ${PYRIS_APPLICATION_YML_FILE}:/config/application.yml:ro + - ${PYRIS_LLM_CONFIG_YML_FILE}:/config/llm_config.yml:ro + networks: + - pyris + + nginx: + extends: + file: ./nginx.yml + service: nginx + restart: always + volumes: + - type: bind + source: ${NGINX_PROXY_SSL_CERTIFICATE_PATH:-./nginx/certs/pyris-nginx+4.pem} + target: "/certs/fullchain.pem" + - type: bind + source: ${NGINX_PROXY_SSL_CERTIFICATE_KEY_PATH:-./nginx/certs/pyris-nginx+4-key.pem} + target: "/certs/priv_key.pem" + networks: + - pyris + +networks: + pyris: + driver: "bridge" + name: pyris \ No newline at end of file diff --git a/docker/pyris.yml b/docker/pyris.yml new file mode 100644 index 00000000..e38877be --- /dev/null +++ b/docker/pyris.yml @@ -0,0 +1,24 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Pyris base service +# ---------------------------------------------------------------------------------------------------------------------- + +services: + pyris-app: + container_name: pyris-app + build: + context: .. + dockerfile: Dockerfile + pull: true + environment: + APPLICATION_YML_PATH: "/config/application.yml" + LLM_CONFIG_PATH: "/config/llm_config.yml" + expose: + - "8000" + networks: + - pyris + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + +networks: + pyris: + driver: "bridge" + name: pyris \ No newline at end of file