From 168866c81b974ec162c67d0623ea13cec0ca3aea Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sat, 11 Oct 2025 13:27:50 +0100 Subject: [PATCH] feat: optimize docker compose for dashboard and api (#578) * chore: use turbo for build * fix docker compose file and remove backend * chore: update .dockerignore to include additional files and directories for improved build management * copy everything in both docke filed * refactor: streamline Dockerfile to copy entire dashboard app and remove unnecessary node_modules references * update api to use turbo to start * run ls la for docker files * update cmd commands * update cmds to use node * fix: add redis url to docker compose * fix: reduce memory allocation for dashboard build in Dockerfile * debug dashboard build * update api docker file * update dashboard * add tsconfig * chore: add echo to dockerfile * fix: change echo to run * chore: install pnpm before switching to non-root user in Dockerfile * refactor: update start commands in package.json and Dockerfiles to use pnpm * install turbo * use turbo 1.10.16 * update turbo config * fix: update CMD in Dockerfile to run the API directly using Node.js * change * add commands to turbo.json * copy over turbo * add workspace * fix: update start commands in package.json to specify ports for dashboard and API * publish docker images * add dev branch * update compose * build only for amd64 platform * refactor: update docker-compose to use pre-built images and remove build context * coolify-dockertest * point to latest * chore: update docker-compose to use specific version tags for API and dashboard images * remove envs for docker file * 0.1.3 * use dynamic env * 0.1.4 * add PUBLIC_IS_SELFHOSTED=true * chore: update docker-compose to use version 0.1.5 for API and dashboard images * use static for PUBLIC_IS_SELFHOSTED * disable cache for tag * use 0.1.7 * add PUBLIC_IS_SELFHOSTED=true * comment out caching * update docker-compose to use version 0.1.8 for API and dashboard images * update api port * use tag 0.1.9 * add cache * use latest tag in compose file --- .dockerignore | 165 ++++++- .github/workflows/docker-publish.yml | 71 +++ apps/api/Dockerfile | 85 ---- apps/backend/.dockerignore | 2 - apps/backend/.env.example | 20 - apps/backend/Dockerfile | 71 --- apps/backend/certificate.html | 431 ------------------ apps/backend/course.html | 182 -------- apps/backend/fly.toml | 18 - apps/backend/lesson.html | 186 -------- apps/backend/src/config/env.ts | 20 - apps/backend/src/constants/index.ts | 25 - apps/backend/src/middlewares/auth.ts | 44 -- apps/backend/src/routes/course/katex.ts | 41 -- apps/backend/src/routes/mail.ts | 70 --- apps/backend/src/services/mail.ts | 97 ---- apps/backend/src/types/course/index.ts | 39 -- apps/backend/src/types/course/lesson.ts | 25 - apps/backend/src/types/mail.ts | 13 - apps/backend/src/utils/auth/validate-user.ts | 21 - apps/backend/src/utils/certificate.ts | 124 ----- apps/backend/src/utils/cloudflare.ts | 27 -- apps/backend/src/utils/course.ts | 170 ------- apps/backend/src/utils/email.ts | 35 -- apps/backend/src/utils/genUniqueId.ts | 3 - apps/backend/src/utils/lesson.ts | 161 ------- apps/backend/src/utils/mail.ts | 213 --------- apps/backend/src/utils/s3.ts | 56 --- apps/backend/src/utils/supabase.ts | 36 -- apps/backend/src/utils/upload.ts | 24 - apps/backend/vitest.config.ts | 13 - apps/dashboard/Dockerfile | 81 ---- apps/dashboard/src/routes/+layout.server.ts | 2 +- apps/docs/pages/quickstart/self-hosting.mdx | 8 +- docker/.dockerignore | 4 + docker/Dockerfile.api | 31 ++ docker/Dockerfile.dashboard | 60 +++ .../docker-compose.yaml | 45 +- docker/docker-push.sh | 62 +++ docker/docs/README.md | 215 +++++++++ docker/docs/USAGE.md | 141 ++++++ docker/docs/commands.md | 90 ++++ package.json | 5 +- turbo.json | 4 + 44 files changed, 848 insertions(+), 2388 deletions(-) create mode 100644 .github/workflows/docker-publish.yml delete mode 100644 apps/api/Dockerfile delete mode 100644 apps/backend/.dockerignore delete mode 100644 apps/backend/.env.example delete mode 100644 apps/backend/Dockerfile delete mode 100644 apps/backend/certificate.html delete mode 100644 apps/backend/course.html delete mode 100644 apps/backend/fly.toml delete mode 100644 apps/backend/lesson.html delete mode 100644 apps/backend/src/config/env.ts delete mode 100644 apps/backend/src/constants/index.ts delete mode 100644 apps/backend/src/middlewares/auth.ts delete mode 100644 apps/backend/src/routes/course/katex.ts delete mode 100644 apps/backend/src/routes/mail.ts delete mode 100644 apps/backend/src/services/mail.ts delete mode 100644 apps/backend/src/types/course/index.ts delete mode 100644 apps/backend/src/types/course/lesson.ts delete mode 100644 apps/backend/src/types/mail.ts delete mode 100644 apps/backend/src/utils/auth/validate-user.ts delete mode 100644 apps/backend/src/utils/certificate.ts delete mode 100644 apps/backend/src/utils/cloudflare.ts delete mode 100644 apps/backend/src/utils/course.ts delete mode 100644 apps/backend/src/utils/email.ts delete mode 100644 apps/backend/src/utils/genUniqueId.ts delete mode 100644 apps/backend/src/utils/lesson.ts delete mode 100644 apps/backend/src/utils/mail.ts delete mode 100644 apps/backend/src/utils/s3.ts delete mode 100644 apps/backend/src/utils/supabase.ts delete mode 100644 apps/backend/src/utils/upload.ts delete mode 100644 apps/backend/vitest.config.ts delete mode 100644 apps/dashboard/Dockerfile create mode 100644 docker/.dockerignore create mode 100644 docker/Dockerfile.api create mode 100644 docker/Dockerfile.dashboard rename docker-compose.yaml => docker/docker-compose.yaml (50%) create mode 100755 docker/docker-push.sh create mode 100644 docker/docs/README.md create mode 100644 docker/docs/USAGE.md create mode 100644 docker/docs/commands.md diff --git a/.dockerignore b/.dockerignore index 4efc3dc2..5865c997 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,18 +1,165 @@ -Dockerfile +# Docker files +Dockerfile* .dockerignore + +# Git .git .gitignore .gitattributes +.gitmodules + +# Documentation README.md -.npmrc +CHANGELOG.md +CONTRIBUTING.md +CODE_OF_CONDUCT.md +LICENSE +*.md + +# IDE and Editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Node.js +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm +.yarn-integrity +.pnpm-debug.log* + +# Build outputs +**/dist +**/build +**/.next +**/.svelte-kit +**/coverage +**/.nyc_output +**/test-results +**/playwright-report +**/playwright/.cache + +# Dependencies +**/node_modules +**/.pnpm-store +.pnpm-store/* + +# Environment files +**/.env +**/.env.local +**/.env.development +**/.env.test +**/.env.production +**/.env.staging + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Turbo +**/.turbo + +# Testing +**/cypress +**/cypress.config.js +**/cypress.config.ts +**/test-results +**/playwright-report +**/playwright/.cache + +# Package managers (keep pnpm-lock.yaml for builds) +package-lock.json +yarn.lock + +# Config files that aren't needed in container .prettierrc .eslintrc.cjs .graphqlrc .editorconfig -.vscode -node_modules -build -package -**/.env - -.pnpm-store/* \ No newline at end of file +.npmrc \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..65753dbb --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,71 @@ +name: Publish Docker Images + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version tag (e.g., v1.0.0)" + required: false + default: "latest" + +env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'classroomio' }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: api + dockerfile: ./docker/Dockerfile.api + image: api + - name: dashboard + dockerfile: ./docker/Dockerfile.dashboard + image: dashboard + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push ${{ matrix.name }} + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache + cache-to: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache,mode=max + platforms: linux/amd64 + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile deleted file mode 100644 index fb59e5c3..00000000 --- a/apps/api/Dockerfile +++ /dev/null @@ -1,85 +0,0 @@ -FROM node:slim AS app - -# Add build arguments -ARG PUBLIC_SUPABASE_ANON_KEY -ARG PUBLIC_SUPABASE_URL -ARG CLOUDFLARE_BUCKET_DOMAIN -ARG CLOUDFLARE_ACCESS_KEY -ARG CLOUDFLARE_SECRET_ACCESS_KEY -ARG CLOUDFLARE_ACCOUNT_ID -ARG CLOUDFLARE_BUCKET_ID -ARG SMTP_HOST -ARG SMTP_USER -ARG SMTP_PASSWORD -ARG SMTP_PORT -ARG SMTP_SENDER -ARG ZOHO_TOKEN -ARG CLOUDFLARE_RENDERING_API_KEY -ARG OPENAPI_URL - -RUN mkdir /app -WORKDIR /app - -# Install pnpm and healthcheck tools -RUN npm install -g pnpm && \ - apt-get update && \ - apt-get install -y curl wget && \ - rm -rf /var/lib/apt/lists/* - -# Install chrome stable from sources, then remove it. Why? The -# npm install of `puppeteer` brings its own bundle chromium build, -# and puppeteer releases are only guaranteed to work with that version. -# -# But the bundled chromium implicitly needs a bunch of shared libs on -# the host. It's a little tedious to find and maintain that list; but -# the official apt distro of `google-chrome-stable` should bring the right -# set along. So do that, but immediately uninstall (to free up layer space). -# -# This is a little brittle, since the puppeteer chrome could in theory diverge -# from the official apt chrome's shared lib deps. But it works.. -# RUN apt-get update \ -# && apt-get install curl gnupg -y \ -# && curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ -# && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ -# && apt-get update \ -# && apt-get install google-chrome-stable -y --no-install-recommends \ -# && apt-get remove google-chrome-stable -y \ -# && rm -rf /var/lib/apt/lists/* - -# Add emoji fonts. -# Source: https://gist.github.com/win0err/9d8c7f0feabdfe8a4c9787b02c79ac51 -# RUN mkdir ~/.fonts/ && \ -# wget https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf -O ~/.fonts/AppleColorEmoji.ttf - -COPY ./package.json /app -RUN pnpm install -COPY . /app - -# Set environment variables from build args -ENV NODE_ENV=production \ - PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \ - PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \ - CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \ - CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \ - CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \ - CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \ - CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \ - CLOUDFLARE_RENDERING_API_KEY=$CLOUDFLARE_RENDERING_API_KEY \ - SMTP_HOST=$SMTP_HOST \ - SMTP_USER=$SMTP_USER \ - SMTP_PASSWORD=$SMTP_PASSWORD \ - SMTP_PORT=$SMTP_PORT \ - SMTP_SENDER=$SMTP_SENDER \ - ZOHO_TOKEN=$ZOHO_TOKEN \ - OPENAPI_URL=$OPENAPI_URL \ - PORT=3002 - -RUN pnpm build - -EXPOSE 3002 - -# Add healthcheck -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3002 || exit 1 - -CMD ["pnpm", "start"] diff --git a/apps/backend/.dockerignore b/apps/backend/.dockerignore deleted file mode 100644 index 5171c540..00000000 --- a/apps/backend/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -npm-debug.log \ No newline at end of file diff --git a/apps/backend/.env.example b/apps/backend/.env.example deleted file mode 100644 index 0f24be11..00000000 --- a/apps/backend/.env.example +++ /dev/null @@ -1,20 +0,0 @@ -## Supabase - DB -PUBLIC_SUPABASE_ANON_KEY= -PUBLIC_SUPABASE_URL= - -## Cloudflare - Video uploads -# Not required - https://cdn.example.com -CLOUDFLARE_BUCKET_DOMAIN= - -CLOUDFLARE_ACCESS_KEY= -CLOUDFLARE_SECRET_ACCESS_KEY= -CLOUDFLARE_ACCOUNT_ID= - -# Email -SMTP_HOST= -SMTP_USER= -SMTP_PASSWORD= -SMTP_PORT= - -# Sentry DNS -SENTRY_DNS= \ No newline at end of file diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile deleted file mode 100644 index dff48ecc..00000000 --- a/apps/backend/Dockerfile +++ /dev/null @@ -1,71 +0,0 @@ -FROM node:slim AS app - -# Add build arguments -ARG PUBLIC_SUPABASE_ANON_KEY -ARG PUBLIC_SUPABASE_URL -ARG CLOUDFLARE_BUCKET_DOMAIN -ARG CLOUDFLARE_ACCESS_KEY -ARG CLOUDFLARE_SECRET_ACCESS_KEY -ARG CLOUDFLARE_ACCOUNT_ID -ARG CLOUDFLARE_BUCKET_ID -ARG SMTP_HOST -ARG SMTP_USER -ARG SMTP_PASSWORD -ARG SMTP_PORT -ARG SMTP_SENDER - -RUN mkdir /app -WORKDIR /app - -# Install pnpm -RUN npm install -g pnpm - -# Install chrome stable from sources, then remove it. Why? The -# npm install of `puppeteer` brings its own bundle chromium build, -# and puppeteer releases are only guaranteed to work with that version. -# -# But the bundled chromium implicitly needs a bunch of shared libs on -# the host. It's a little tedious to find and maintain that list; but -# the official apt distro of `google-chrome-stable` should bring the right -# set along. So do that, but immediately uninstall (to free up layer space). -# -# This is a little brittle, since the puppeteer chrome could in theory diverge -# from the official apt chrome's shared lib deps. But it works.. -# RUN apt-get update \ -# && apt-get install curl gnupg -y \ -# && curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ -# && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ -# && apt-get update \ -# && apt-get install google-chrome-stable -y --no-install-recommends \ -# && apt-get remove google-chrome-stable -y \ -# && rm -rf /var/lib/apt/lists/* - -# Add emoji fonts. -# Source: https://gist.github.com/win0err/9d8c7f0feabdfe8a4c9787b02c79ac51 -# RUN mkdir ~/.fonts/ && \ -# wget https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf -O ~/.fonts/AppleColorEmoji.ttf - -COPY ./package.json /app -RUN pnpm install -COPY . /app - -# Set environment variables from build args -ENV NODE_ENV=production \ - PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \ - PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \ - CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \ - CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \ - CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \ - CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \ - CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \ - SMTP_HOST=$SMTP_HOST \ - SMTP_USER=$SMTP_USER \ - SMTP_PASSWORD=$SMTP_PASSWORD \ - SMTP_PORT=$SMTP_PORT \ - PORT=3002 - -RUN pnpm build - -EXPOSE 3002 - -CMD ["pnpm", "start"] diff --git a/apps/backend/certificate.html b/apps/backend/certificate.html deleted file mode 100644 index 26392dbf..00000000 --- a/apps/backend/certificate.html +++ /dev/null @@ -1,431 +0,0 @@ - - - - - - - - - - Certificate - - - - - -
-
- -

${orgName}

-
- -
-

This certificate is awarded to

-

${studentName}

-
- -
-

has succesfully completed training on

-

${courseName}

-
- -
-
-

${dateIssued}

-

Date Issued

-
- -
-

-

Signature

-
-
- -
${courseDescription}
-
- - - - - - - - - - - - - - - - - - - - - diff --git a/apps/backend/course.html b/apps/backend/course.html deleted file mode 100644 index ca9560e8..00000000 --- a/apps/backend/course.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - Lesson Note - - - - - -
-

React js for Beginners

-

By TheSpace

-
- -
- -
-

- 32 -

-

The Yams

-

By The Space

-
- -
-
-

Lesson Note: Introduction and Counselling

-

Course: The Ultimate Guide to relocating to the UK via Study Path

-

Lesson Overview:

-

This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via - Study Path." In this session, we will provide an overview of the course and discuss the importance of - effective counseling in the relocation process.

-

Learning Objectives:

-

By the end of this lesson, students should be able to:

-
    -
  • Understand the goals and structure of the course.
  • -
  • Recognize the significance of counseling in the relocation process.
  • -
  • Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.
  • -
-

Lesson Content:

-
    -
  1. -

    Introduction to the Course

    -
      -
    • Briefly introduce the course title and explain its relevance to students' interests.
    • -
    • Highlight the main topics and modules covered throughout the course.
    • -
    -
  2. -
  3. -

    Importance of Counseling

    -
      -
    • Define the role of counseling in the relocation process.
    • -
    • Discuss how counseling can assist students in making informed decisions regarding their studies and - life in the UK.
    • -
    • Explain the benefits of seeking professional guidance during the relocation journey.
    • -
    -
  4. -
  5. -

    Steps for Relocating to the UK via Study Path

    -
      -
    • Provide an overview of the necessary steps involved in relocating to the UK for study purposes.
    • -
    • Discuss the importance of thorough planning, research, and documentation in the relocation process. -
    • -
    • Highlight key points such as visa applications, university admissions, accommodation arrangements, and - financial considerations.
    • -
    -
  6. -
  7. -

    Q&A and Discussion Time

    -
      -
    • Allocate time for students to ask questions, provide comments, or share personal experiences.
    • -
    • Encourage active participation and foster a supportive learning environment.
    • -
    -
  8. -
-

Conclusion:

-

In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study - Path" and emphasized the significance of counseling in the relocation process. We explored the essential - steps involved in relocating to the UK and highlighted the importance of thorough planning and research. - Students should now have a better understanding of the course objectives and overall expectations. In the next - session, we will delve deeper into the specific requirements for studying in the UK and provide detailed - guidance on university admissions.

- -
- -
-

Extra Resources

-
-

Slide Link:

Open Here -
- -

Video Link:

- - -
-
- - - -
- - - \ No newline at end of file diff --git a/apps/backend/fly.toml b/apps/backend/fly.toml deleted file mode 100644 index 009283bc..00000000 --- a/apps/backend/fly.toml +++ /dev/null @@ -1,18 +0,0 @@ -# fly.toml app configuration file generated for classroomio-server on 2023-07-27T21:01:09+01:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = "classroomio-server" -primary_region = "lhr" - -[build] - dockerfile = "Dockerfile" - -[http_service] - internal_port = 3002 - force_https = true - auto_stop_machines = true - auto_start_machines = true - min_machines_running = 0 - processes = ["app"] diff --git a/apps/backend/lesson.html b/apps/backend/lesson.html deleted file mode 100644 index b31ff14c..00000000 --- a/apps/backend/lesson.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - Lesson Note - - - - - -
-

React js for Beginners

-

By TheSpace

-
- -
- -
-

- 32 -

-

The Yams

-

By The Space

-
- -
-
-

Lesson Note: Introduction and Counselling

-

Course: The Ultimate Guide to relocating to the UK via Study Path

-

Lesson Overview:

-

This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via - Study Path." In this session, we will provide an overview of the course and discuss the importance of - effective counseling in the relocation process.

-

Learning Objectives:

-

By the end of this lesson, students should be able to:

-
    -
  • Understand the goals and structure of the course.
  • -
  • Recognize the significance of counseling in the relocation process.
  • -
  • Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.
  • -
-

Lesson Content:

-
    -
  1. -

    Introduction to the Course

    -
      -
    • Briefly introduce the course title and explain its relevance to students' interests.
    • -
    • Highlight the main topics and modules covered throughout the course.
    • -
    -
  2. -
  3. -

    Importance of Counseling

    -
      -
    • Define the role of counseling in the relocation process.
    • -
    • Discuss how counseling can assist students in making informed decisions regarding their studies and - life in the UK.
    • -
    • Explain the benefits of seeking professional guidance during the relocation journey.
    • -
    -
  4. -
  5. -

    Steps for Relocating to the UK via Study Path

    -
      -
    • Provide an overview of the necessary steps involved in relocating to the UK for study purposes.
    • -
    • Discuss the importance of thorough planning, research, and documentation in the relocation process. -
    • -
    • Highlight key points such as visa applications, university admissions, accommodation arrangements, and - financial considerations.
    • -
    -
  6. -
  7. -

    Q&A and Discussion Time

    -
      -
    • Allocate time for students to ask questions, provide comments, or share personal experiences.
    • -
    • Encourage active participation and foster a supportive learning environment.
    • -
    -
  8. -
-

Conclusion:

-

In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study - Path" and emphasized the significance of counseling in the relocation process. We explored the essential - steps involved in relocating to the UK and highlighted the importance of thorough planning and research. - Students should now have a better understanding of the course objectives and overall expectations. In the next - session, we will delve deeper into the specific requirements for studying in the UK and provide detailed - guidance on university admissions.

- -
- -
-

Extra Resources

-
-

Slide Link:

Open Here -
- -

Video Link:

- - -
-
- - - -
- - - \ No newline at end of file diff --git a/apps/backend/src/config/env.ts b/apps/backend/src/config/env.ts deleted file mode 100644 index 0e18d88c..00000000 --- a/apps/backend/src/config/env.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; - -const envSchema = z.object({ - PORT: z.string().optional(), - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(), - CLOUDFLARE_ACCESS_KEY: z.string().optional(), - CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(), - CLOUDFLARE_ACCOUNT_ID: z.string().optional(), - PUBLIC_SUPABASE_URL: z.string(), - PUBLIC_SUPABASE_ANON_KEY: z.string(), - ZOHO_TOKEN: z.string().optional(), - SMTP_HOST: z.string().optional(), - SMTP_PORT: z.string().optional(), - SMTP_USER: z.string().optional(), - SMTP_PASSWORD: z.string().optional(), - SMTP_SENDER: z.string().optional() -}); - -export const env = envSchema.parse(process.env); diff --git a/apps/backend/src/constants/index.ts b/apps/backend/src/constants/index.ts deleted file mode 100644 index 9db00bc9..00000000 --- a/apps/backend/src/constants/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { env } from '$src/config/env'; - -const { - CLOUDFLARE_BUCKET_DOMAIN, - CLOUDFLARE_ACCESS_KEY, - CLOUDFLARE_SECRET_ACCESS_KEY, - CLOUDFLARE_ACCOUNT_ID -} = env; - -export const CLOUDFLARE = { - CONFIGS: { - BUCKET_DOMAIN: CLOUDFLARE_BUCKET_DOMAIN!, - ACCESS_KEY: CLOUDFLARE_ACCESS_KEY!, - SECRET_ACCESS_KEY: CLOUDFLARE_SECRET_ACCESS_KEY!, - ACCOUNT_ID: CLOUDFLARE_ACCOUNT_ID! - }, - R2: { - BUCKET: 'videos', - BUCKET_DOMAIN: CLOUDFLARE_ACCOUNT_ID - ? `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com` - : CLOUDFLARE_BUCKET_DOMAIN!, - PRESIGN_EXPIRATION_TIME: 60 * 60, // 1 hour - DOWNLOAD_EXPIRATION_TIME: 60 * 60 * 3 // 3 hours - } -} as const; diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts deleted file mode 100644 index 4c6d9aa2..00000000 --- a/apps/backend/src/middlewares/auth.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Context, Next } from 'hono'; - -import { validateUser } from '$src/utils/auth/validate-user'; - -export const authMiddleware = async (c: Context, next: Next) => { - try { - const authHeader = c.req.header('authorization'); - - if (!authHeader) { - return c.json( - { - success: false, - message: 'No authorization header provided' - }, - 401 - ); - } - - // Extract the token from "Bearer " - const token = authHeader.split(' ')[1]; - - if (!token) { - return c.json( - { - success: false, - message: 'Invalid authorization header format' - }, - 401 - ); - } - - const user = await validateUser(token); - c.set('user', user); // Attach the user to the context - await next(); - } catch (error) { - return c.json( - { - success: false, - message: 'Unauthorized' - }, - 401 - ); - } -}; diff --git a/apps/backend/src/routes/course/katex.ts b/apps/backend/src/routes/course/katex.ts deleted file mode 100644 index 49f8d70e..00000000 --- a/apps/backend/src/routes/course/katex.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Context, Hono } from 'hono'; - -import katex from 'katex'; -import { z } from 'zod'; - -export const katexRouter = new Hono(); - -katexRouter.get('/', async (c: Context) => { - try { - const original = c.req.raw.url; - - const latexString = original.split('?')[1].replaceAll('+', '+').replaceAll('&space;', ''); - - const html = katex.renderToString(latexString, { - output: 'mathml', - throwOnError: false - }); - - return c.html(html); - } catch (error) { - if (error instanceof z.ZodError) { - return c.json( - { - success: false, - error: 'Validation error', - details: error.errors - }, - 400 - ); - } - - console.error('Error rendering LaTeX:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred' - }, - 500 - ); - } -}); diff --git a/apps/backend/src/routes/mail.ts b/apps/backend/src/routes/mail.ts deleted file mode 100644 index 539886db..00000000 --- a/apps/backend/src/routes/mail.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Context, Hono } from 'hono'; -import { sendWithNodemailer, sendWithZoho } from '$src/services/mail'; - -import { ZSendEmailValidation } from '$src/types/mail'; -import { env } from '$src/config/env'; -import { z } from 'zod'; - -export const mailRouter = new Hono(); - -mailRouter.post('/send', async (c: Context) => { - try { - const data = await c.req.json(); - const validatedData = ZSendEmailValidation.parse(data); - - const results = await Promise.all( - validatedData.map(async (emailData) => { - try { - const res = env.ZOHO_TOKEN - ? await sendWithZoho(emailData) - : await sendWithNodemailer(emailData); - - console.log('Email status:', res); - return res; - } catch (error) { - console.error('Error sending email:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }; - } - }) - ); - - const hasErrors = results.some((result) => !result.success); - if (hasErrors) { - return c.json( - { - success: false, - error: 'Some emails failed to send', - details: results - }, - 500 - ); - } - - return c.json({ success: true, details: results }); - } catch (error) { - if (error instanceof z.ZodError) { - return c.json( - { - success: false, - error: 'Validation error', - details: error.errors - }, - 400 - ); - } - - console.error('Error sending emails:', error); - return c.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }, - 500 - ); - } -}); diff --git a/apps/backend/src/services/mail.ts b/apps/backend/src/services/mail.ts deleted file mode 100644 index 2d46422e..00000000 --- a/apps/backend/src/services/mail.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { extractNameAndEmail, withEmailTemplate } from '$src/utils/mail'; -import { nodemailerTransporter, zohoClient } from '$src/utils/email'; - -import { Hono } from 'hono'; -import type { TSendEmailValidation } from '$src/types/mail'; -import type { Transporter } from 'nodemailer'; -import { env } from '$src/config/env'; - -export const mailRouter = new Hono(); - -interface EmailResponse { - success: boolean; - error?: string; - details?: unknown; -} - -let transporter: Transporter | null; - -nodemailerTransporter().then((t: Transporter | null) => { - transporter = t; -}); - -export async function sendWithNodemailer( - emailData: TSendEmailValidation[0] -): Promise { - const { from, to, subject, content, replyTo } = emailData; - - if (!transporter) { - return { - success: false, - error: 'Email transporter not initialized' - }; - } - - try { - const result = await transporter.sendMail({ - from: from || env.SMTP_SENDER, - to, - subject, - replyTo, - html: withEmailTemplate(content) - }); - - return { - success: true, - details: result - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }; - } -} - -export async function sendWithZoho(emailData: TSendEmailValidation[0]): Promise { - const { from, to, subject, content } = emailData; - - const fromData = extractNameAndEmail(from || ''); - - if (!fromData) { - return { - success: false, - error: 'Invalid from data' - }; - } - - try { - const result = await zohoClient.sendMail({ - from: { - address: fromData.email, - name: fromData.name - }, - to: [ - { - email_address: { - address: to - } - } - ], - subject, - htmlbody: content - }); - - return { - success: true, - details: result - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - details: error - }; - } -} diff --git a/apps/backend/src/types/course/index.ts b/apps/backend/src/types/course/index.ts deleted file mode 100644 index 4ccb7062..00000000 --- a/apps/backend/src/types/course/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE } from '$src/constants/upload'; - -import { z } from 'zod'; - -export * from './lesson'; - -export const ZCourseDownloadContent = z.object({ - courseTitle: z.string(), - orgName: z.string(), - orgTheme: z.string(), - lessons: z.array( - z.object({ - lessonTitle: z.string(), - lessonNumber: z.string(), - lessonNote: z.string(), - slideUrl: z.string().optional(), - video: z.array(z.string()).optional() - }) - ) -}); - -export type TCourseDownloadContent = z.infer; - -export const ZCoursePresignUrlUpload = z.object({ - fileName: z.string().min(1, 'File name is required'), - fileType: z.enum(ALLOWED_CONTENT_TYPES, { - errorMap: () => ({ - message: `Invalid content type. Allowed types: ${ALLOWED_CONTENT_TYPES.join(', ')}` - }) - }) -}); - -export type TCoursePresignUrlUpload = z.infer; - -export const ZCourseDownloadPresignedUrl = z.object({ - keys: z.array(z.string()).min(1, 'File names array is required') -}); - -export type TCourseDownloadPresignedUrl = z.infer; diff --git a/apps/backend/src/types/course/lesson.ts b/apps/backend/src/types/course/lesson.ts deleted file mode 100644 index 66f24905..00000000 --- a/apps/backend/src/types/course/lesson.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from 'zod'; - -export const ZLessonDownloadContent = z.object({ - title: z.string(), - number: z.string(), - orgName: z.string(), - note: z.string(), - slideUrl: z.string().optional(), - video: z.array(z.string()).optional(), - courseTitle: z.string() -}); - -export type TLessonDownloadContent = z.infer; - -export const ZCertificateDownload = z.object({ - theme: z.string().min(1), - studentName: z.string().min(1), - courseName: z.string().min(1), - courseDescription: z.string().min(1), - orgName: z.string().min(1), - orgLogoUrl: z.string().url().optional(), - facilitator: z.string().optional() -}); - -export type TCertificateDownload = z.infer; diff --git a/apps/backend/src/types/mail.ts b/apps/backend/src/types/mail.ts deleted file mode 100644 index 78116183..00000000 --- a/apps/backend/src/types/mail.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -export const ZSendEmailValidation = z.array( - z.object({ - from: z.string().optional(), - to: z.string().email(), - subject: z.string().min(1), - content: z.string().min(1), - replyTo: z.string().email().optional() - }) -); - -export type TSendEmailValidation = z.infer; diff --git a/apps/backend/src/utils/auth/validate-user.ts b/apps/backend/src/utils/auth/validate-user.ts deleted file mode 100644 index 9ea20f2f..00000000 --- a/apps/backend/src/utils/auth/validate-user.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { User } from '@supabase/supabase-js'; -import { getSupabase } from '../supabase.js'; - -const supabase = getSupabase(); - -export async function validateUser(accessToken: string): Promise { - let user: User | null = null; - - try { - const { data } = await supabase.auth.getUser(accessToken); - user = data.user; - } catch (error) { - console.error(error); - } - - if (!user) { - throw new Error('Unauthenticated user'); - } - - return user; -} diff --git a/apps/backend/src/utils/certificate.ts b/apps/backend/src/utils/certificate.ts deleted file mode 100644 index b9a40eb9..00000000 --- a/apps/backend/src/utils/certificate.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { TCertificateDownload } from '$src/types/course/lesson'; -import { getCloudflarePdfBuffer } from '$src/utils/cloudflare'; - -export interface CertificateData { - theme: string; - studentName: string; - courseName: string; - courseDescription: string; - orgLogoUrl: string; - orgName: string; - facilitator?: string; -} - -export interface CertificateGenerationResult { - success: boolean; - buffer?: Buffer; - error?: string; -} - -export const generateCertificate = async (data: TCertificateDownload) => { - const { html, styles } = generateCertificateHtml(data); - return getCloudflarePdfBuffer(html, styles); -}; - -function generateCertificateHtml(data: TCertificateDownload): { html: string; styles: string } { - const { studentName, courseName, courseDescription, orgLogoUrl, orgName, facilitator } = data; - - const html = ` - - - - Certificate of Completion - - -
-
- -

Certificate of Completion

-
-
-

${studentName}

-

${courseName}

-

${courseDescription}

-
- -
- - - `; - - const styles = ` - body { - font-family: 'Arial', sans-serif; - margin: 0; - padding: 0; - background-color: #f5f5f5; - } - .certificate { - width: 1123px; - height: 794px; - margin: 0 auto; - background-color: white; - position: relative; - box-shadow: 0 0 10px rgba(0,0,0,0.1); - } - .header { - text-align: center; - padding: 20px; - } - .logo { - max-width: 200px; - margin-bottom: 20px; - } - .title { - font-size: 36px; - color: #333; - margin-bottom: 10px; - } - .content { - text-align: center; - padding: 40px; - } - .student-name { - font-size: 48px; - color: #2c3e50; - margin-bottom: 20px; - } - .course-name { - font-size: 24px; - color: #34495e; - margin-bottom: 10px; - } - .description { - font-size: 18px; - color: #7f8c8d; - margin-bottom: 30px; - } - .footer { - position: absolute; - bottom: 40px; - width: 100%; - text-align: center; - } - .signature { - margin-top: 20px; - } - .date { - margin-top: 10px; - color: #95a5a6; - } - `; - const pdf = { - html: html, - styles: styles - }; - - return pdf; -} diff --git a/apps/backend/src/utils/cloudflare.ts b/apps/backend/src/utils/cloudflare.ts deleted file mode 100644 index 9d480803..00000000 --- a/apps/backend/src/utils/cloudflare.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CLOUDFLARE } from '$src/constants'; - -export const getCloudflarePdfBuffer = async (html: string, styles?: string) => { - console.log('Generating PDF with Cloudflare API...'); - try { - const pdfResponse = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE.CONFIGS.ACCOUNT_ID}/browser-rendering/pdf`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${CLOUDFLARE.CONFIGS.ACCESS_KEY}` - }, - body: JSON.stringify({ - html: html, - addStyleTag: [{ content: `${styles}` }] - }) - } - ); - - console.log('PDF response status:', pdfResponse.status); - return pdfResponse; - } catch (error) { - console.error('Error generating PDF:', error); - throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF'); - } -}; diff --git a/apps/backend/src/utils/course.ts b/apps/backend/src/utils/course.ts deleted file mode 100644 index 200b2261..00000000 --- a/apps/backend/src/utils/course.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { TCourseDownloadContent } from '$src/types/course'; -import { marked } from 'marked'; - -function getHtmlTemplate(body: string): string { - return ` - - - - - - - - - - Lesson Note - - - - - ${body} - - - `; -} - -type TLessonBody = TCourseDownloadContent['lessons'][0]; - -function getLessonBody( - { lessonTitle, lessonNumber, lessonNote, slideUrl, video }: TLessonBody, - orgName: string, - orgTheme: string -): string { - const noteHtml = marked.parse(lessonNote); - const showExtraResources = slideUrl || (video && video.length > 0); - return ` -
-

- ${lessonNumber} -

-

${lessonTitle}

-

By ${orgName}

-
- -
-
- ${noteHtml} -
- -
- ${ - showExtraResources - ? ` -
-

Extra Resources

- ${ - slideUrl - ? `
-

Slide Link:

Open Here -
` - : '' - } - ${ - video && video.length > 0 - ? ` -

Video Link:

-
-
    - ${video - .map( - (videoData, index) => ` -
  1. ${videoData}
  2. - ` - ) - .join('')} -
-
- ` - : '' - } -
- ` - : '' - } -
-
- `; -} - -function getCourseBody({ - courseTitle, - orgName, - orgTheme, - lessons -}: TCourseDownloadContent): string { - return ` -
-

${courseTitle}

-

${orgName}

-
-
- ${lessons.map((lesson) => getLessonBody(lesson, orgName, orgTheme)).join('')} -
- `; -} - -async function generateSinglePdfFromHtml( - htmlContent: string, - courseTitle: string, - orgTheme: string -): Promise { - throw 'Download disabled'; -} - -export async function generateCoursePdf(params: TCourseDownloadContent): Promise { - const body = getCourseBody(params); - const html = getHtmlTemplate(body); - console.log(html); - return await generateSinglePdfFromHtml(html, params.courseTitle, params.orgTheme); -} diff --git a/apps/backend/src/utils/email.ts b/apps/backend/src/utils/email.ts deleted file mode 100644 index 3a52bf81..00000000 --- a/apps/backend/src/utils/email.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SendMailClient } from 'zeptomail'; -import type { Transporter } from 'nodemailer'; -import { env } from '$src/config/env'; -import nodemailer from 'nodemailer'; - -export const zohoClient = new SendMailClient({ - url: 'api.zeptomail.eu/', - token: env.ZOHO_TOKEN! -}); - -export const nodemailerTransporter = async (): Promise => { - if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASSWORD) { - console.error('SMTP configuration missing'); - return null; - } - - try { - const transporter = nodemailer.createTransport({ - host: env.SMTP_HOST, - port: parseInt(env.SMTP_PORT || '465', 10), - secure: true, - auth: { - user: env.SMTP_USER, - pass: env.SMTP_PASSWORD - } - }); - - await transporter.verify(); - - return transporter; - } catch (error) { - console.error('Transporter error:', error); - return null; - } -}; diff --git a/apps/backend/src/utils/genUniqueId.ts b/apps/backend/src/utils/genUniqueId.ts deleted file mode 100644 index b45b91f3..00000000 --- a/apps/backend/src/utils/genUniqueId.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function genUniqueId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2); -} diff --git a/apps/backend/src/utils/lesson.ts b/apps/backend/src/utils/lesson.ts deleted file mode 100644 index 4f4c6241..00000000 --- a/apps/backend/src/utils/lesson.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { TLessonDownloadContent } from '$src/types/course/lesson'; -import { marked } from 'marked'; - -function getHtmlTemplate(body: string): string { - return ` - - - - - - - - - - Lesson Note - - - - - ${body} - - - `; -} - -function getLessonBody({ - title, - number, - orgName, - note, - slideUrl, - video, - courseTitle -}: TLessonDownloadContent): string { - const noteHtml = marked.parse(note); - const showExtraResources = slideUrl || (video && video.length > 0); - return ` -
-
-

- ${number} -

-

${title}

-

By ${orgName}

-
- -
-
- ${noteHtml} -
- -
- ${ - showExtraResources - ? ` -
-

Extra Resources

- ${ - slideUrl - ? `
-

Slide Link:

Open Here -
` - : '' - } - ${ - video && video.length > 0 - ? ` -

Video Link:

-
-
    - ${video - .map( - (videoData, index) => ` -
  1. ${videoData}
  2. - ` - ) - .join('')} -
-
- ` - : '' - } -
- ` - : '' - } -
-
- -
- `; -} - -async function generateSinglePdfFromHtml( - htmlContent: string, - courseTitle: string -): Promise { - throw 'Download disabled'; -} - -export async function generateLessonPdf(params: TLessonDownloadContent): Promise { - const body = getLessonBody(params); - const html = getHtmlTemplate(body); - console.log(html); - return await generateSinglePdfFromHtml(html, params.courseTitle); -} diff --git a/apps/backend/src/utils/mail.ts b/apps/backend/src/utils/mail.ts deleted file mode 100644 index 99525b6e..00000000 --- a/apps/backend/src/utils/mail.ts +++ /dev/null @@ -1,213 +0,0 @@ -interface FromData { - name: string; - email: string; -} - -// format: "ClassroomIO Developers (via ClassroomIO.com)" -export function extractNameAndEmail(str: string): FromData | undefined { - // Use regular expressions to match the name and email - const regex = /"(.*?)"\s+<\s*(.*?)@(.*?)\s*>/; - const match = str.match(regex); - - if (match) { - // Extract the name and email from the match groups - const name = match[1]; - const email = match[2] + '@' + match[3]; - return { name, email }; - } else { - // Return undefined if the format doesn't match - return { name: str, email: str }; - } -} - -export const withEmailTemplate = (content: string): string => - ` - - - - - - - - - - - -
- - ClassroomIO Logo -
-
- ${content} -
- - - - - -`; diff --git a/apps/backend/src/utils/s3.ts b/apps/backend/src/utils/s3.ts deleted file mode 100644 index 56b56d05..00000000 --- a/apps/backend/src/utils/s3.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - DeleteObjectCommand, - DeleteObjectCommandInput, - GetObjectCommand, - GetObjectCommandInput, - PutObjectCommand, - PutObjectCommandInput, - S3Client -} from '@aws-sdk/client-s3'; - -import { CLOUDFLARE } from '$src/constants'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; - -export type GetSignedUrlParameters = Parameters; - -export const s3Client = new S3Client({ - region: 'auto', - endpoint: `https://${CLOUDFLARE.CONFIGS.ACCOUNT_ID}.r2.cloudflarestorage.com`, - credentials: { - accessKeyId: CLOUDFLARE.CONFIGS.ACCESS_KEY, - secretAccessKey: CLOUDFLARE.CONFIGS.SECRET_ACCESS_KEY - } -}); - -export async function uploadToS3( - params: PutObjectCommandInput -): Promise<{ success: boolean; error?: string }> { - try { - await s3Client.send(new PutObjectCommand(params)); - return { success: true }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -} - -export async function getFromS3( - params: GetObjectCommandInput -): Promise<{ success: boolean; data?: any; error?: string }> { - try { - const data = await s3Client.send(new GetObjectCommand(params)); - return { success: true, data }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -} - -export async function deleteFromS3( - params: DeleteObjectCommandInput -): Promise<{ success: boolean; error?: string }> { - try { - await s3Client.send(new DeleteObjectCommand(params)); - return { success: true }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -} diff --git a/apps/backend/src/utils/supabase.ts b/apps/backend/src/utils/supabase.ts deleted file mode 100644 index cb0e461f..00000000 --- a/apps/backend/src/utils/supabase.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SupabaseClient, createClient } from '@supabase/supabase-js'; - -import { env } from '$src/config/env'; - -const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = env; - -let supabase: SupabaseClient | null = null; - -interface SupabaseConfig { - supabaseConfig: { - url: string; - anonKey: string; - }; -} - -export function getSupabase(config?: SupabaseConfig): SupabaseClient { - if (supabase) return supabase; - - const finalConfig = config ?? { - supabaseConfig: { - url: PUBLIC_SUPABASE_URL, - anonKey: PUBLIC_SUPABASE_ANON_KEY - } - }; - - try { - supabase = createClient(finalConfig.supabaseConfig.url, finalConfig.supabaseConfig.anonKey, { - auth: { persistSession: false } - }); - } catch (error) { - console.error('Error starting supabase', error); - throw error; - } - - return supabase; -} diff --git a/apps/backend/src/utils/upload.ts b/apps/backend/src/utils/upload.ts deleted file mode 100644 index 99cd0198..00000000 --- a/apps/backend/src/utils/upload.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { nanoid } from 'nanoid'; -import path from 'path'; - -/** - * Safely extracts the file extension from a filename. - * Returns an empty string if no extension is found. - */ -export function getExtension(filename: string): string { - if (!filename || typeof filename !== 'string') return ''; - const ext = path.extname(filename).split('.'); - return ext.length > 1 ? ext[ext.length - 1] : ''; -} - -/** - * Generates a unique file key for storage, preserving the file extension. - * Throws an error if the filename is invalid or has no extension. - */ -export function generateFileKey(fileName: string): string { - const ext = getExtension(fileName); - if (!ext) { - throw new Error('Invalid file name or missing file extension'); - } - return `${nanoid()}.${ext}`; -} diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts deleted file mode 100644 index f29f8b2a..00000000 --- a/apps/backend/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - exclude: ['node_modules/', 'dist/'] - } - } -}); diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile deleted file mode 100644 index e97737b2..00000000 --- a/apps/dashboard/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# ----------- Build Stage ----------- -FROM node:18.18.0-alpine AS builder - -# Add build arguments -ARG PUBLIC_SUPABASE_URL -ARG PUBLIC_SUPABASE_ANON_KEY -ARG PRIVATE_SUPABASE_SERVICE_ROLE -ARG PRIVATE_APP_HOST -ARG PRIVATE_APP_SUBDOMAINS -ARG UNSPLASH_API_KEY -ARG OPENAI_API_KEY -ARG PUBLIC_SERVER_URL -ARG PUBLIC_IP_REGISTRY_KEY - -WORKDIR /cio - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate - -# Copy monorepo files -COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./ -COPY apps/dashboard/package.json apps/dashboard/ -COPY packages/tsconfig ./packages/tsconfig -COPY packages/shared ./packages/shared - -# Install all dependencies (monorepo context) -RUN pnpm install - -# Copy the rest of the dashboard app -COPY apps/dashboard ./apps/dashboard - -# Set env vars for SvelteKit to use Node adapter -ENV PUBLIC_IS_SELFHOSTED=true \ - PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \ - PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \ - PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \ - PRIVATE_APP_HOST=$PRIVATE_APP_HOST \ - PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \ - UNSPLASH_API_KEY=$UNSPLASH_API_KEY \ - OPENAI_API_KEY=$OPENAI_API_KEY \ - PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \ - PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY - -# Build the dashboard app -WORKDIR /cio/apps/dashboard -RUN pnpm install -RUN NODE_OPTIONS=--max-old-space-size=4096 pnpm build - -# ----------- Production Stage ----------- -FROM node:18.18.0-alpine AS runner - -WORKDIR /app - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate - -# Copy only production node_modules for dashboard -COPY --from=builder ./cio/node_modules ./node_modules -COPY --from=builder ./cio/package.json ./package.json -COPY --from=builder ./cio/apps/dashboard/node_modules ./apps/dashboard/node_modules -COPY --from=builder ./cio/apps/dashboard/package.json ./apps/dashboard/package.json -COPY --from=builder ./cio/apps/dashboard/build ./apps/dashboard/build - -# Expose the port (default for SvelteKit Node adapter) -EXPOSE 3000 - -# Set environment variables for production -ENV NODE_ENV=production \ - PUBLIC_IS_SELFHOSTED=true \ - PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \ - PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \ - PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \ - PRIVATE_APP_HOST=$PRIVATE_APP_HOST \ - PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \ - UNSPLASH_API_KEY=$UNSPLASH_API_KEY \ - OPENAI_API_KEY=$OPENAI_API_KEY \ - PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \ - PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY - -# Start the app -CMD ["pnpm", "dashboard:start"] \ No newline at end of file diff --git a/apps/dashboard/src/routes/+layout.server.ts b/apps/dashboard/src/routes/+layout.server.ts index 0f22797b..aa6b499a 100644 --- a/apps/dashboard/src/routes/+layout.server.ts +++ b/apps/dashboard/src/routes/+layout.server.ts @@ -1,8 +1,8 @@ import { getSupabase, supabase } from '$lib/utils/functions/supabase'; import type { CurrentOrg } from '$lib/utils/types/org'; -import { PUBLIC_IS_SELFHOSTED } from '$env/static/public'; import type { MetaTagsProps } from 'svelte-meta-tags'; +import { PUBLIC_IS_SELFHOSTED } from '$env/static/public'; import { blockedSubdomain } from '$lib/utils/constants/app'; import { dev } from '$app/environment'; import { env } from '$env/dynamic/private'; diff --git a/apps/docs/pages/quickstart/self-hosting.mdx b/apps/docs/pages/quickstart/self-hosting.mdx index 0c82065f..6e38e64f 100644 --- a/apps/docs/pages/quickstart/self-hosting.mdx +++ b/apps/docs/pages/quickstart/self-hosting.mdx @@ -140,8 +140,8 @@ docker compose up --build The services will be available at: -- Dashboard: http://localhost:3000 -- Backend: http://localhost:3002 +- Dashboard: http://localhost:3082 +- Backend: http://localhost:3081 ### Service Configuration @@ -149,7 +149,7 @@ The docker-compose file sets up two services: #### Backend Service -- Runs on port 3002 +- Runs on port 3081 - Uses Node.js slim image - Mounts the api code directory - Requires environment variables for: @@ -159,7 +159,7 @@ The docker-compose file sets up two services: #### Dashboard Service -- Runs on port 3000 +- Runs on port 3082 - Uses Node.js Alpine image - Uses pnpm for package management - Requires environment variables for: diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000..5c017482 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,4 @@ +# This .dockerignore is a symlink reference +# The actual .dockerignore is in the root directory +# Docker will use the root .dockerignore when building from the docker/ directory + diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 00000000..6d8cd06d --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,31 @@ +FROM node:20.19.3-slim AS app + +RUN mkdir /app +WORKDIR /app + +# Install pnpm, turbo and healthcheck tools +RUN corepack enable && corepack prepare pnpm@latest --activate +RUN npm install -g turbo@1.10.16 + +COPY package.json ./ +COPY pnpm-lock.yaml ./ +COPY pnpm-workspace.yaml ./ +COPY turbo.json ./ +COPY packages/tsconfig ./packages/tsconfig +COPY packages/shared ./packages/shared + +# Copy the api package.json +COPY apps/api/package.json ./apps/api/package.json + +RUN pnpm install + +# Copy app source +COPY . . + +RUN pnpm build --filter @cio/api + +EXPOSE 3081 + +RUN ls -la + +CMD ["pnpm", "api:start"] diff --git a/docker/Dockerfile.dashboard b/docker/Dockerfile.dashboard new file mode 100644 index 00000000..c1e1fe2e --- /dev/null +++ b/docker/Dockerfile.dashboard @@ -0,0 +1,60 @@ +# ----------- Build Stage ----------- +FROM node:20.19.3-alpine AS builder + +WORKDIR /cio +# Install pnpm globally so it's available in the final container +RUN npm install -g pnpm + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy everything (filtered by .dockerignore) +COPY package.json ./ +COPY pnpm-lock.yaml ./ +COPY pnpm-workspace.yaml ./ +COPY turbo.json ./ +COPY packages/tsconfig ./packages/tsconfig +COPY packages/shared ./packages/shared + +# Copy the api package.json +COPY apps/dashboard ./apps/dashboard +COPY apps/api ./apps/api + +# Install all dependencies (monorepo context) +RUN pnpm install + +RUN ls -la + +# Build the dashboard app +RUN NODE_OPTIONS="--max-old-space-size=3072" PUBLIC_IS_SELFHOSTED=true pnpm build --filter @cio/dashboard + +# ----------- Production Stage ----------- +FROM node:20.19.3-alpine AS runner + +WORKDIR /app + +# Install pnpm and turbo before switching to non-root user +RUN corepack enable && corepack prepare pnpm@latest --activate +RUN npm install -g turbo@1.10.16 + +# Don't run production as root +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 cio-user +USER cio-user + +# Copy the entire dashboard app (including build output) +COPY --from=builder --chown=cio-user:nodejs ./cio/apps/dashboard ./apps/dashboard +COPY --from=builder --chown=cio-user:nodejs ./cio/node_modules ./node_modules +COPY --from=builder --chown=cio-user:nodejs ./cio/package.json ./package.json +COPY --from=builder --chown=cio-user:nodejs ./cio/turbo.json ./turbo.json +COPY --from=builder --chown=cio-user:nodejs ./cio/pnpm-workspace.yaml ./pnpm-workspace.yaml + +# Expose the port (default for SvelteKit Node adapter) +EXPOSE 3082 + +# Set environment variables for production +ENV NODE_ENV=production \ + PUBLIC_IS_SELFHOSTED=true + +# Start the app +CMD ["pnpm", "dashboard:start"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker/docker-compose.yaml similarity index 50% rename from docker-compose.yaml rename to docker/docker-compose.yaml index 1be1f9e6..9fe3eef5 100644 --- a/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -8,28 +8,9 @@ version: "3.8" services: api: - build: - context: ./apps/api - dockerfile: Dockerfile - args: - - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY} - - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL} - - CLOUDFLARE_BUCKET_DOMAIN=${CLOUDFLARE_BUCKET_DOMAIN} - - CLOUDFLARE_ACCESS_KEY=${CLOUDFLARE_ACCESS_KEY} - - CLOUDFLARE_SECRET_ACCESS_KEY=${CLOUDFLARE_SECRET_ACCESS_KEY} - - CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} - - CLOUDFLARE_BUCKET_ID=${CLOUDFLARE_BUCKET_ID} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - SMTP_PORT=${SMTP_PORT} + image: classroomio/api:latest ports: - - "3002:3002" - volumes: - # Node modules (optional - for faster development) - - ./apps/api/node_modules:/app/node_modules - # Build output directory (if you want to access built files) - - ./apps/api/dist:/app/dist + - "3081:3081" environment: - NODE_ENV=production # Supabase - Database @@ -46,27 +27,13 @@ services: - SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_PORT=${SMTP_PORT} - SMTP_SENDER=${SMTP_SENDER} + # Redis + - REDIS_URL=${REDIS_URL} dashboard: - build: - context: . - dockerfile: ./apps/dashboard/Dockerfile - args: - - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL} - - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY} - - PRIVATE_SUPABASE_SERVICE_ROLE=${PRIVATE_SUPABASE_SERVICE_ROLE} - - PRIVATE_APP_HOST=${PRIVATE_APP_HOST} - - PRIVATE_APP_SUBDOMAINS=${PRIVATE_APP_SUBDOMAINS} - - UNSPLASH_API_KEY=${UNSPLASH_API_KEY} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - PUBLIC_SERVER_URL=${PUBLIC_SERVER_URL} - - PUBLIC_IP_REGISTRY_KEY=${PUBLIC_IP_REGISTRY_KEY} + image: classroomio/dashboard:latest ports: - - "3000:3000" - volumes: - - .:/app - - /app/node_modules - - /app/apps/dashboard/node_modules + - "3082:3082" environment: - PUBLIC_IS_SELFHOSTED=true # Supabase - Database diff --git a/docker/docker-push.sh b/docker/docker-push.sh new file mode 100755 index 00000000..3666f240 --- /dev/null +++ b/docker/docker-push.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Docker Hub Push Script for ClassroomIO +# This script builds and pushes the API and Dashboard images to Docker Hub + +set -e # Exit on error + +# Configuration +DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME:-classroomio}" # Change this to your Docker Hub username +VERSION="${VERSION:-latest}" # Can be overridden with VERSION env var + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}================================${NC}" +echo -e "${BLUE}ClassroomIO Docker Hub Publisher${NC}" +echo -e "${BLUE}================================${NC}" +echo "" + +# Build and push API +echo -e "${GREEN}Building API image...${NC}" +docker build \ + -f docker/Dockerfile.api \ + -t ${DOCKERHUB_USERNAME}/api:${VERSION} \ + -t ${DOCKERHUB_USERNAME}/api:latest \ + . + +echo -e "${GREEN}Pushing API image to Docker Hub...${NC}" +docker push ${DOCKERHUB_USERNAME}/api:${VERSION} +docker push ${DOCKERHUB_USERNAME}/api:latest + +echo -e "${GREEN}✓ API image published successfully!${NC}" +echo "" + +# Build and push Dashboard +echo -e "${GREEN}Building Dashboard image...${NC}" +docker build \ + -f docker/Dockerfile.dashboard \ + -t ${DOCKERHUB_USERNAME}/dashboard:${VERSION} \ + -t ${DOCKERHUB_USERNAME}/dashboard:latest \ + . + +echo -e "${GREEN}Pushing Dashboard image to Docker Hub...${NC}" +docker push ${DOCKERHUB_USERNAME}/dashboard:${VERSION} +docker push ${DOCKERHUB_USERNAME}/dashboard:latest + +echo -e "${GREEN}✓ Dashboard image published successfully!${NC}" + +# Summary +echo -e "${BLUE}================================${NC}" +echo -e "${GREEN}All images published successfully!${NC}" +echo -e "${BLUE}================================${NC}" +echo "" +echo "API Image: ${DOCKERHUB_USERNAME}/api:${VERSION}" +echo "Dashboard Image: ${DOCKERHUB_USERNAME}/dashboard:${VERSION}" +echo "" +echo "To pull these images:" +echo " docker pull ${DOCKERHUB_USERNAME}/api:${VERSION}" +echo " docker pull ${DOCKERHUB_USERNAME}/dashboard:${VERSION} diff --git a/docker/docs/README.md b/docker/docs/README.md new file mode 100644 index 00000000..5db22022 --- /dev/null +++ b/docker/docs/README.md @@ -0,0 +1,215 @@ +# Publishing ClassroomIO Docker Images to Docker Hub + +This guide explains how to publish the ClassroomIO API and Dashboard Docker images to Docker Hub. + +## Prerequisites + +1. **Docker Hub Account**: Create an account at https://hub.docker.com if you don't have one +2. **Docker Installed**: Make sure Docker is installed and running on your machine +3. **Docker Hub Access Token**: Create a personal access token at https://hub.docker.com/settings/security + +## Method 1: Manual Publishing (Local) + +### Step 1: Login to Docker Hub + +```bash +docker login +# Enter your Docker Hub username and password/token when prompted +``` + +### Step 2: Build and Push Images + +Use the provided script to build and push both images: + +```bash +# Default (uses 'classroomio' as username and 'latest' as version) +./docker/docker-push.sh + +# Custom username +DOCKERHUB_USERNAME=your-username ./docker/docker-push.sh + +# Specific version +VERSION=v0.1.0 ./docker/docker-push.sh + +# Both custom username and version +DOCKERHUB_USERNAME=classroomio VERSION=v0.1.0 ./docker/docker-push.sh +``` + +### Step 3: Verify Images + +Check your Docker Hub repository at: +- https://hub.docker.com/r/classroomio/api +- https://hub.docker.com/r/classroomio/dashboard + +## Method 2: GitHub Actions (Automated) + +### Setup (One-time) + +1. **Add Docker Hub Secrets to GitHub**: + - Go to your GitHub repository → Settings → Secrets and variables → Actions + - Add two secrets: + - `DOCKERHUB_USERNAME`: Your Docker Hub username + - `DOCKERHUB_TOKEN`: Your Docker Hub access token + +2. **Push the workflow file** (already created at `.github/workflows/docker-publish.yml`) + +### Publishing via GitHub Actions + +The workflow will automatically publish images when: + +1. **Push to main/master branch**: Creates a `latest` tag +2. **Create a version tag**: Creates versioned tags +3. **Manual trigger**: Use the Actions tab in GitHub + +#### Create a version tag and push: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +#### Manual trigger from GitHub: + +1. Go to Actions tab in your repository +2. Select "Publish Docker Images" workflow +3. Click "Run workflow" +4. Enter optional version tag (or leave default 'latest') + +## Method 3: Individual Image Building + +### Build API Image + +```bash +docker build -f docker/Dockerfile.api -t classroomio/api:latest . +docker push classroomio/api:latest +``` + +### Build Dashboard Image + +```bash +docker build -f docker/Dockerfile.dashboard -t classroomio/dashboard:latest . +docker push classroomio/dashboard:latest +``` + +## Using Published Images + +### Pull Images + +```bash +docker pull classroomio/api:latest +docker pull classroomio/dashboard:latest +``` + +### Update docker-compose.yml + +Instead of building locally, use the published images: + +```yaml +services: + api: + image: classroomio/api:latest + # Remove the 'build' section + ports: + - "3081:3081" + environment: + # ... your environment variables + + dashboard: + image: classroomio/dashboard:latest + # Remove the 'build' section + ports: + - "3082:3082" + environment: + # ... your environment variables +``` + +## Versioning Strategy + +### Recommended Tags: + +- `latest`: Most recent stable build +- `v0.1.0`: Specific semantic version +- `v1.0`: Minor version +- `v1`: Major version +- `main`: Latest main branch build +- `dev`: Development builds + +### Example: Publishing a new version + +```bash +# Build with multiple tags +docker build -f docker/Dockerfile.api \ + -t classroomio/api:latest \ + -t classroomio/api:v1.2.0 \ + -t classroomio/api:v1.2 \ + -t classroomio/api:v1 \ + . + +# Push all tags +docker push classroomio/api:latest +docker push classroomio/api:v1.2.0 +docker push classroomio/api:v1.2 +docker push classroomio/api:v1 +``` + +## Image Sizes and Build Times + +- **API Image**: ~400-600 MB (Node.js Alpine + dependencies) +- **Dashboard Image**: ~600-800 MB (Node.js Alpine + SvelteKit build) +- **Build Time**: 5-10 minutes per image (depending on your machine) + +## Multi-Architecture Support + +The GitHub Actions workflow builds for both `linux/amd64` and `linux/arm64` platforms, making the images compatible with: +- x86_64 servers +- ARM-based servers (like AWS Graviton) +- Apple Silicon Macs (M1/M2/M3) + +## Troubleshooting + +### "unauthorized: authentication required" +- Run `docker login` again +- Check your Docker Hub token hasn't expired + +### "denied: requested access to the resource is denied" +- Make sure you're pushing to your own username +- Repository might need to be created first on Docker Hub + +### Build failures +- Check Dockerfile syntax +- Ensure all build arguments are provided +- Check available disk space: `docker system df` + +### Clean up old images +```bash +# Remove dangling images +docker image prune + +# Remove all unused images +docker image prune -a +``` + +## Security Best Practices + +1. **Never commit secrets** to the repository +2. **Use Docker Hub access tokens** instead of passwords +3. **Enable 2FA** on your Docker Hub account +4. **Scan images** for vulnerabilities: + ```bash + docker scan classroomio/api:latest + ``` +5. **Use specific version tags** in production, not `latest` + +## Support + +For issues with publishing, please check: +- Docker Hub status: https://status.docker.com +- GitHub Actions logs in your repository +- Docker daemon logs: `docker info` + +## References + +- [Docker Hub Documentation](https://docs.docker.com/docker-hub/) +- [GitHub Actions for Docker](https://docs.github.com/en/actions/publishing-packages/publishing-docker-images) +- [Docker Build Documentation](https://docs.docker.com/engine/reference/commandline/build/) + diff --git a/docker/docs/USAGE.md b/docker/docs/USAGE.md new file mode 100644 index 00000000..23738f18 --- /dev/null +++ b/docker/docs/USAGE.md @@ -0,0 +1,141 @@ +# Docker Usage Guide + +This directory contains all Docker-related files for the ClassroomIO project. + +## 📁 Directory Structure + +``` +docker/ +├── README.md # Full publishing guide +├── USAGE.md # This file - quick usage guide +├── commands.md # Quick command reference +├── docker-compose.yaml # Docker Compose configuration +├── docker-push.sh # Script to publish images to Docker Hub +├── Dockerfile.api # API service Dockerfile +└── Dockerfile.dashboard # Dashboard service Dockerfile +``` + +## 🚀 Quick Start + +### Using Docker Compose (Recommended) + +```bash +# From the project root +cd docker +docker-compose up -d + +# Or from project root +docker-compose -f docker/docker-compose.yaml up -d +``` + +### Building Individual Images + +```bash +# From project root +docker build -f docker/Dockerfile.api -t api . +docker build -f docker/Dockerfile.dashboard -t dashboard . +``` + +## 📦 Publishing to Docker Hub + +```bash +# From project root +./docker/docker-push.sh + +# With custom options +DOCKERHUB_USERNAME=your-username VERSION=v1.0.0 ./docker/docker-push.sh +``` + +## 🔧 Common Commands + +### Start Services +```bash +cd docker && docker-compose up -d +``` + +### Stop Services +```bash +cd docker && docker-compose down +``` + +### View Logs +```bash +cd docker && docker-compose logs -f +``` + +### Rebuild Services +```bash +cd docker && docker-compose up -d --build +``` + +## 📚 Documentation + +- **README.md**: Complete Docker Hub publishing guide +- **commands.md**: Quick command reference +- **GitHub Actions**: Automated publishing via `.github/workflows/docker-publish.yml` + +## 🌍 Environment Variables + +Copy `.env.example` to `.env` in the project root and configure: + +```bash +cp ../.env.example ../.env +``` + +Required variables are listed in `docker-compose.yaml`. + +## 🔗 Published Images + +- **API**: `classroomio/api` or `classroomio/api` +- **Dashboard**: `classroomio/dashboard` or `classroomio/dashboard` + +Pull from Docker Hub: +```bash +docker pull classroomio/api:latest +docker pull classroomio/dashboard:latest +``` + +## 💡 Tips + +1. **Always run builds from project root** - the context needs to be the entire monorepo +2. **Use docker-compose** for local development +3. **Use published images** for production deployments +4. **Check logs** if services fail to start: `docker-compose logs -f` + +## 🐛 Troubleshooting + +### Services won't start +```bash +# Check logs +docker-compose logs -f + +# Rebuild from scratch +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +### Port conflicts +```bash +# Check if ports are in use +lsof -i :3081 # API +lsof -i :3082 # Dashboard + +# Change ports in docker-compose.yaml if needed +``` + +### Build failures +```bash +# Clean up Docker +docker system prune -a + +# Try building again +docker-compose build --no-cache +``` + +## 📞 Support + +For more detailed information, see: +- `README.md` - Full Docker Hub publishing guide +- `commands.md` - Quick command reference +- Project main README for general setup diff --git a/docker/docs/commands.md b/docker/docs/commands.md new file mode 100644 index 00000000..c8b88f5f --- /dev/null +++ b/docker/docs/commands.md @@ -0,0 +1,90 @@ +# Quick Docker Commands Reference + +## Login to Docker Hub +```bash +docker login +``` + +## Build Images Locally + +### API +```bash +docker build -f ./docker/Dockerfile.api -t classroomio/api:latest . +``` + +### Dashboard +```bash +docker build -f ./docker/Dockerfile.dashboard -t classroomio/dashboard:latest . +``` + +## Push to Docker Hub + +### API +```bash +docker push classroomio/api:latest +``` + +### Dashboard +```bash +docker push classroomio/dashboard:latest +``` + +## All in One Script +```bash +# Make script executable (first time only) +chmod +x docker-push.sh + +# Run the script +./docker-push.sh + +# With custom username +DOCKERHUB_USERNAME=your-username ./docker-push.sh + +# With version tag +VERSION=v1.0.0 ./docker-push.sh +``` + +## Pull Published Images +```bash +docker pull classroomio/api:latest +docker pull classroomio/dashboard:latest +``` + +## Run Published Images +```bash +# API +docker run -d -p 3081:3081 \ + -e PUBLIC_SUPABASE_ANON_KEY=your_key \ + -e PUBLIC_SUPABASE_URL=your_url \ + classroomio/api:latest + +# Dashboard +docker run -d -p 3082:3082 \ + -e PUBLIC_SUPABASE_ANON_KEY=your_key \ + -e PUBLIC_SUPABASE_URL=your_url \ + classroomio/dashboard:latest +``` + +## Useful Commands + +### Check local images +```bash +docker images | grep classroomio +``` + +### Remove local images +```bash +docker rmi classroomio/api:latest +docker rmi classroomio/dashboard:latest +``` + +### View image details +```bash +docker inspect classroomio/api:latest +``` + +### Check image size +```bash +docker images classroomio/api:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" +``` + diff --git a/package.json b/package.json index 48f485c9..e08d353e 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "version": "0.1.1", "scripts": { "build": "turbo run build", + "start": "turbo run start", "dev": "turbo prepare --concurrency 13 && turbo run dev", "lint": "turbo run lint", "clean": "turbo clean && rm -rf node_modules", "format": "prettier . --write", "release": "standard-version", "ci": "cypress run", - "dashboard:build": "cd apps/dashboard && pnpm i && PUBLIC_IS_SELFHOSTED=true NODE_OPTIONS='--max-old-space-size=20384' pnpm build", - "dashboard:start": "cd apps/dashboard && node build", + "dashboard:start": "PORT=3082 pnpm start --filter @cio/dashboard", + "api:start": "PORT=3081 pnpm start --filter @cio/api", "supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all" }, "license": "MIT", diff --git a/turbo.json b/turbo.json index 0cec25d5..a6f4535a 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,10 @@ "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", ".svelte-kit/**", ".vercel/**", "dist/**"] }, + "start": { + "cache": false, + "persistent": true + }, "lint": { "dependsOn": ["^build"] },