From ae018c51409289b2f33e07d290e717c6fb93cff1 Mon Sep 17 00:00:00 2001 From: King-Koufan <148069250+Koufan-De-King@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:37:41 +0100 Subject: [PATCH] Set up docker containerization of frontend for deployment (#64) * chore: refactored code base to get arguments at runtime * feat: added compose file for better testing * refactored docker runtime code * refactored docker runtime code to support multiple variables * chore: refactored code to accept multiple env variables at runtime passed using docker-compose * chore: removed gh-pages deployment entirely * testing container deployment * testing container deployment * testing container deployment * testing container deployment * fix: fixed lint formatting and failing tests * fix: cleanup after testing * fix: cleanup after testing * testing staging environment backend url * testing staging environment backend url * testing staging environment backend url * testing staging environment backend url * testing staging environment backend url --------- Co-authored-by: Stephane Segning Lambou --- .docker/app/entrypoint.sh | 5 ++ .docker/app/nginx/conf.d/default.conf | 13 +++++ .docker/app/nginx/conf.d/gzip.conf | 8 +++ .../100-init-project-env-variables.sh | 8 +++ .docker/app/nginx/nginx.conf | 26 ++++++++++ .env.example | 1 + .github/workflows/deploy.yml | 2 + .github/workflows/gh-pages-deploy.yml | 52 ------------------- Dockerfile | 35 +++++++++++++ compose.yml | 8 +++ dockerfile | 24 --------- .../__tests__/apiSercice.test.ts | 11 ++-- src/services/keyManagement/apiService.ts | 6 ++- src/shared/projectEnvVariables.ts | 19 +++++++ vite.config.ts | 16 ++++++ 15 files changed, 153 insertions(+), 81 deletions(-) create mode 100755 .docker/app/entrypoint.sh create mode 100644 .docker/app/nginx/conf.d/default.conf create mode 100644 .docker/app/nginx/conf.d/gzip.conf create mode 100755 .docker/app/nginx/init-scripts/100-init-project-env-variables.sh create mode 100644 .docker/app/nginx/nginx.conf create mode 100644 .env.example delete mode 100644 .github/workflows/gh-pages-deploy.yml create mode 100644 Dockerfile create mode 100644 compose.yml delete mode 100644 dockerfile create mode 100644 src/shared/projectEnvVariables.ts diff --git a/.docker/app/entrypoint.sh b/.docker/app/entrypoint.sh new file mode 100755 index 0000000..e00593f --- /dev/null +++ b/.docker/app/entrypoint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +set -ex + +sh /docker-entrypoint.sh "$@" diff --git a/.docker/app/nginx/conf.d/default.conf b/.docker/app/nginx/conf.d/default.conf new file mode 100644 index 0000000..595ab46 --- /dev/null +++ b/.docker/app/nginx/conf.d/default.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name 127.0.0.1 localhost; + + root /usr/share/nginx/html; + index index.html index.html; + + charset utf-8; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/.docker/app/nginx/conf.d/gzip.conf b/.docker/app/nginx/conf.d/gzip.conf new file mode 100644 index 0000000..b9bc062 --- /dev/null +++ b/.docker/app/nginx/conf.d/gzip.conf @@ -0,0 +1,8 @@ + # Compression. + gzip on; + gzip_disable msie6; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 0; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf; + diff --git a/.docker/app/nginx/init-scripts/100-init-project-env-variables.sh b/.docker/app/nginx/init-scripts/100-init-project-env-variables.sh new file mode 100755 index 0000000..b1e1640 --- /dev/null +++ b/.docker/app/nginx/init-scripts/100-init-project-env-variables.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +set -ex + +projectEnvVariables=$(ls -t /usr/share/nginx/html/assets/projectEnvVariables*.js | head -n1) +envsubst < "$projectEnvVariables" > ./projectEnvVariables_temp +cp ./projectEnvVariables_temp "$projectEnvVariables" +rm ./projectEnvVariables_temp diff --git a/.docker/app/nginx/nginx.conf b/.docker/app/nginx/nginx.conf new file mode 100644 index 0000000..f52ffda --- /dev/null +++ b/.docker/app/nginx/nginx.conf @@ -0,0 +1,26 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + types { + application/manifest+json webmanifest; + } + + default_type application/octet-stream; + + sendfile on; + + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c262822 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_BACKEND_URL="FALLBACK_URL" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6c5d797..5319c43 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,5 +30,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . + build-args: | + VITE_BACKEND_URL=/api push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/.github/workflows/gh-pages-deploy.yml b/.github/workflows/gh-pages-deploy.yml deleted file mode 100644 index 0e3aad5..0000000 --- a/.github/workflows/gh-pages-deploy.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ['main'] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow one concurrent deployment -concurrency: - group: 'pages' - cancel-in-progress: true - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - name: Install dependencies - run: npm ci - - name: Build - run: npm run build - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload dist folder - path: './dist' - deployment_id: 'WEBANK_USERAPP' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c79a610 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +RUN npm run build + +FROM nginx:alpine + +ARG VITE_BACKEND_URL + +ENV VITE_BACKEND_URL=${VITE_BACKEND_URL} + +ARG PORT=80 +ENV NGINX_PORT=${PORT} +ENV NGINX_HOST=localhost + +EXPOSE ${PORT} + +COPY .docker/app/nginx/nginx.conf /etc/nginx/nginx.conf +COPY .docker/app/nginx/conf.d/ /etc/nginx/conf.d/ +COPY .docker/app/entrypoint.sh /entrypoint.sh +COPY .docker/app/nginx/init-scripts/ /docker-entrypoint.d/ + +WORKDIR /usr/share/nginx/html + +COPY --from=builder /app/dist ./ \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..98934f2 --- /dev/null +++ b/compose.yml @@ -0,0 +1,8 @@ +services: + test-frontend: + build: + context: ./ + ports: + - "2456:80" + environment: + VITE_BACKEND_URL: 'http://localhost:8555' \ No newline at end of file diff --git a/dockerfile b/dockerfile deleted file mode 100644 index ced4858..0000000 --- a/dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# Use official Node.js image as a base image -FROM node:16-alpine - -# Set working directory -WORKDIR /app - -# Copy package.json and install dependencies -COPY package*.json ./ -RUN npm install - -# Copy the rest of the application -COPY . . - -# Build the app for production -RUN npm run build - -# Set environment variable for serving the app -ENV NODE_ENV=production - -# Expose the port that the app will run on -EXPOSE 5173 - -# Command to start the app and bind to 0.0.0.0 -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/src/services/keyManagement/__tests__/apiSercice.test.ts b/src/services/keyManagement/__tests__/apiSercice.test.ts index 99bfd6b..15b44ad 100644 --- a/src/services/keyManagement/__tests__/apiSercice.test.ts +++ b/src/services/keyManagement/__tests__/apiSercice.test.ts @@ -1,6 +1,9 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { sendOTP } from "../apiService"; +import { getProjectEnvVariables } from "../../../shared/projectEnvVariables.ts"; + +const { envVariables } = getProjectEnvVariables(); describe("sendOTP", () => { let mock: MockAdapter; // Explicitly declare the type @@ -18,7 +21,7 @@ describe("sendOTP", () => { const jwtToken = "valid-token"; mock - .onPost("http://localhost:8080/api/registration") + .onPost(`${envVariables.VITE_BACKEND_URL}/registration`) .reply(200, { message: "OTP sent" }); const response = await sendOTP(fullPhoneNumber, jwtToken); @@ -30,7 +33,7 @@ describe("sendOTP", () => { const fullPhoneNumber = "1234567890"; const jwtToken = "valid-token"; - mock.onPost("http://localhost:8080/api/registration").reply(500); + mock.onPost(`${envVariables.VITE_BACKEND_URL}/registration`).reply(500); await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( "Failed to send OTP", @@ -41,7 +44,7 @@ describe("sendOTP", () => { const fullPhoneNumber = "1234567890"; const jwtToken = "invalid-token"; - mock.onPost("http://localhost:8080/api/registration").reply(401); + mock.onPost(`${envVariables.VITE_BACKEND_URL}/registration`).reply(401); await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( "Failed to send OTP", @@ -56,7 +59,7 @@ describe("sendOTP", () => { const fullPhoneNumber = "1234567890"; const jwtToken = "valid-token"; - mock.onPost("http://localhost:8080/api/registration").networkError(); + mock.onPost(`${envVariables.VITE_BACKEND_URL}/registration`).networkError(); await expect(sendOTP(fullPhoneNumber, jwtToken)).rejects.toThrow( "Failed to send OTP", diff --git a/src/services/keyManagement/apiService.ts b/src/services/keyManagement/apiService.ts index 3f129b1..41e490a 100644 --- a/src/services/keyManagement/apiService.ts +++ b/src/services/keyManagement/apiService.ts @@ -1,4 +1,7 @@ import axios from "axios"; +import { getProjectEnvVariables } from "../../shared/projectEnvVariables.ts"; + +const { envVariables } = getProjectEnvVariables(); export const sendOTP = async (fullPhoneNumber: string, jwtToken: string) => { // Create the request object with both phone number and public key const requestBody = { @@ -10,8 +13,9 @@ export const sendOTP = async (fullPhoneNumber: string, jwtToken: string) => { }; try { + // Send the post request to the backend const response = await axios.post( - "http://localhost:8080/api/registration", + `${envVariables.VITE_BACKEND_URL}/registration`, requestBody, { headers }, ); diff --git a/src/shared/projectEnvVariables.ts b/src/shared/projectEnvVariables.ts new file mode 100644 index 0000000..7743606 --- /dev/null +++ b/src/shared/projectEnvVariables.ts @@ -0,0 +1,19 @@ +type ProjectEnvVariablesType = Pick; + +const projectEnvVariables: ProjectEnvVariablesType = { + VITE_BACKEND_URL: "${VITE_BACKEND_URL}", +}; + +interface ProjectEnvVariables { + envVariables: ProjectEnvVariablesType; +} + +export const getProjectEnvVariables = (): ProjectEnvVariables => { + return { + envVariables: { + VITE_BACKEND_URL: !projectEnvVariables.VITE_BACKEND_URL.includes("VITE_") + ? projectEnvVariables.VITE_BACKEND_URL + : import.meta.env.VITE_BACKEND_URL, + }, + }; +}; diff --git a/vite.config.ts b/vite.config.ts index f59ef77..6dce640 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,22 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ base: './', + build: { + rollupOptions: { + output: { + format: 'es', + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + manualChunks(id) { + if (/projectEnvVariables.ts/.test(id)) { + return 'projectEnvVariables' + } + }, + }, + }, + }, plugins: [react(), VitePWA({ strategies: 'injectManifest', srcDir: 'src',