diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..85920ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +node_modules +dist +dist-ssr +.git +.gitignore +.env +.env.* +!.env.example +*.log +logs +*.local +.vscode +.idea +.DS_Store +Dockerfile +.dockerignore +docker-compose*.yml +README.md +vercel.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fdb604b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1 + +# ---------- deps: install once, cached until package.json changes ---------- +FROM node:22.12-alpine3.20 AS deps +WORKDIR /app +# package-lock.json* is optional: this repo gitignores it. Commit it to get +# reproducible installs (npm install honours a lockfile when present). +COPY package.json package-lock.json* ./ +RUN npm install --no-audit --no-fund + +# ---------- build: tsc -b && vite build ---------- +FROM node:22.12-alpine3.20 AS build +WORKDIR /app + +# Vite inlines import.meta.env.VITE_* at build time, so these are build args, +# NOT runtime env. Changing them requires a rebuild. +ARG VITE_API_BASE_URL +ARG VITE_RECAPTCHA_SITE_KEY +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} \ + VITE_RECAPTCHA_SITE_KEY=${VITE_RECAPTCHA_SITE_KEY} \ + NODE_ENV=production + +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN test -n "$VITE_API_BASE_URL" || (echo "VITE_API_BASE_URL build arg is required" >&2; exit 1) \ + && npm run build + +# ---------- runtime: static files only, no node, non-root ---------- +FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime + +# Config baked into the image; no bind mounts. +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html + +# Image already runs as uid 101 (nginx); stated explicitly. +USER nginx +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..21f7b8d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + web: + build: + context: . + target: runtime + args: + # Build-time only: Vite bakes these into the bundle. + VITE_API_BASE_URL: ${VITE_API_BASE_URL} + VITE_RECAPTCHA_SITE_KEY: ${VITE_RECAPTCHA_SITE_KEY} + restart: unless-stopped + # Coolify's proxy reaches this over the internal network; no host ports. + expose: + - "8080" + environment: + # Consumed by Coolify to route the domain at the right container port. + SERVICE_URL_WEB: ${SERVICE_URL_WEB:-} + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp + - /var/cache/nginx + cap_drop: + - ALL + cap_add: + - CHOWN + - SETGID + - SETUID + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"] + interval: 30s + timeout: 3s + start_period: 5s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..1450f3e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,44 @@ +server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Coolify/Traefik terminates TLS upstream; trust its forwarded scheme only. + absolute_redirect off; + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_types text/plain text/css application/javascript application/json + image/svg+xml font/woff2 application/wasm; + + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + # Vite emits content-hashed filenames here -> cache hard. + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + location /fonts/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=2592000"; + } + + # Never cache the shell: it points at the hashed bundles. + location = /index.html { + add_header Cache-Control "no-store"; + } + + # SPA fallback, mirrors vercel.json rewrites. + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/App.tsx b/src/App.tsx index 03cdb3a..cc1203c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,33 +30,33 @@ import Dashboard from "./Pages/Dashboard"; import EditFaq from "./Pages/EditFAQ"; import EditFeedback from "./Pages/EditFeedback"; import EditLowongan from "./Pages/EditLowongan"; -import HomeInternPage from "./Pages/Internify/intern/HomeInternPage"; -import { InternLayout, MentorLayout } from "./Pages/Internify/layout"; -import ProjectsPage from "./Pages/Internify/intern/ProjectsPage"; -import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage"; -import HomeMentorPage from "./Pages/Internify/mentor/HomeMentorPage"; -import MentorProjectsPage from "./Pages/Internify/mentor/BaseProjectsPage"; -import MentorCertificatePage from "./Pages/Internify/mentor/CertificateListPage"; -import MentorInternPage from "./Pages/Internify/mentor/InternsPage"; -import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage"; -import InternifyLogin from "./Pages/Internify/LoginInternify" -import FAQPage from "./Pages/Internify/FAQpage"; -import NotificationList from "./Pages/Internify/NotificationList"; -import SettingsPage from "./Pages/Internify/SettingsPage"; -import TaskSubmission from "./Pages/Internify/intern/TaskSubmission"; -import SertificateList from "./Pages/Internify/intern/InternCertList"; -import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage"; -import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission"; +import HomeInternPage from "./Pages/internify/intern/HomeInternPage"; +import { InternLayout, MentorLayout } from "./Pages/internify/layout"; +import ProjectsPage from "./Pages/internify/intern/ProjectsPage"; +import ProjectDetailPage from "./Pages/internify/intern/ProjectDetailPage"; +import HomeMentorPage from "./Pages/internify/mentor/HomeMentorPage"; +import MentorProjectsPage from "./Pages/internify/mentor/BaseProjectsPage"; +import MentorCertificatePage from "./Pages/internify/mentor/CertificateListPage"; +import MentorInternPage from "./Pages/internify/mentor/InternsPage"; +import MentorDetailProject from "./Pages/internify/mentor/ProjectsPage"; +import InternifyLogin from "./Pages/internify/LoginInternify" +import FAQPage from "./Pages/internify/FAQpage"; +import NotificationList from "./Pages/internify/NotificationList"; +import SettingsPage from "./Pages/internify/SettingsPage"; +import TaskSubmission from "./Pages/internify/intern/TaskSubmission"; +import SertificateList from "./Pages/internify/intern/InternCertList"; +import MentorProjectsDetailPage from "./Pages/internify/mentor/MentorProjectDetailPage"; +import ViewInternSubmission from "./Pages/internify/mentor/ViewInternSubmission"; import ProtectedRoutes from "./Pages/utils/ProtectedRoute"; import AdminMentorPage from "./Pages/MentorPage"; import CreateMentorForm from "./Pages/CreateMentorForm"; import EditMentorForm from "./Pages/EditMentorForm"; import NotFoundPage from "./Pages/NotFoundPage"; -import VerificationCert from "./Pages/Internify/VerificationCert"; +import VerificationCert from "./Pages/internify/VerificationCert"; -const InternCertificatePage = lazy(() => import("./Pages/Internify/intern/InternCertDetail")); -const MentorCertForm = lazy(() => import("./Pages/Internify/mentor/CertificateForm")); -const MentorCertResult = lazy(() => import("./Pages/Internify/mentor/CertificateResult")); +const InternCertificatePage = lazy(() => import("./Pages/internify/intern/InternCertDetail")); +const MentorCertForm = lazy(() => import("./Pages/internify/mentor/CertificateForm")); +const MentorCertResult = lazy(() => import("./Pages/internify/mentor/CertificateResult")); function App() { return (