diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b5868e2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +.git +.github +.claude +.moon/cache +node_modules +**/node_modules +.env +.env.* +!.env.example +Dockerfile +docker-compose.yml +Machine_Learning/dataset +Machine_Learning/dataset.zip +Machine_Learning/dataset_*.zip +Machine_Learning/model +Machine_Learning/best_model/* +!Machine_Learning/best_model/best_model.keras +apps/web/dist +apps/api/dist +__pycache__ +*.pyc +.pytest_cache +.venv +venv +**/.venv +**/venv diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..fecbc75 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,98 @@ +name: Build and Deploy + +on: + push: + branches: + - main + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ghcr.io/${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + service: + - name: web + dockerfile: apps/web/Dockerfile + - name: api + dockerfile: apps/api/Dockerfile + - name: ml + dockerfile: apps/ml-service/Dockerfile + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}/${{ matrix.service.name }} + tags: | + type=ref,event=branch + type=sha + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.service.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.service.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.service.name }} + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + permissions: + contents: read + packages: read + steps: + - name: Deploy to VPS + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + port: ${{ secrets.VPS_PORT || 22 }} + script: | + set -e + cd /opt/ZeaVis-Edu + git pull origin main + + cat > .env << 'ENVEOF' + GITHUB_REPOSITORY=${{ github.repository }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + SESSION_SECRET=${{ secrets.SESSION_SECRET }} + WEB_APP_URL=https://zeavisedu.asepharyana.tech + ML_SERVICE_URL=https://ml.zeavisedu.asepharyana.tech + ENVEOF + + docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + docker network create app-shared-net 2>/dev/null || true + docker compose pull + docker compose up -d + docker compose ps + docker compose ps | grep -q "zeavis-web.*Up" || exit 1 + docker compose ps | grep -q "zeavis-api.*Up" || exit 1 + docker compose ps | grep -q "zeavis-ml.*Up" || exit 1 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..a8d07c9 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,21 @@ +FROM oven/bun:1.3.14 AS deps +WORKDIR /app + +COPY package.json bun.lock bunfig.toml tsconfig.base.json ./ +COPY apps/api/package.json apps/api/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/shared/package.json packages/shared/package.json +RUN bun install --frozen-lockfile --production + +FROM oven/bun:1.3.14 AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV API_PORT=3000 + +COPY --from=deps /app/node_modules ./node_modules +COPY package.json bun.lock bunfig.toml tsconfig.base.json ./ +COPY apps/api apps/api +COPY packages/shared packages/shared + +EXPOSE 3000 +CMD ["bun", "apps/api/src/index.ts"] diff --git a/apps/ml-service/Dockerfile b/apps/ml-service/Dockerfile new file mode 100644 index 0000000..9c855f1 --- /dev/null +++ b/apps/ml-service/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim AS runner + +WORKDIR /app +ENV PYTHONUNBUFFERED=1 +ENV MODEL_PATH=/app/model/best_model.keras +ENV MODEL_INPUT_SIZE=224 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +COPY apps/ml-service/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY apps/ml-service ./ +COPY Machine_Learning/best_model/best_model.keras /app/model/best_model.keras + +EXPOSE 8000 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..1eade72 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,19 @@ +FROM oven/bun:1.3.14 AS builder +WORKDIR /app + +COPY package.json bun.lock bunfig.toml tsconfig.base.json ./ +COPY packages/shared/package.json packages/shared/package.json +COPY apps/web/package.json apps/web/package.json +COPY apps/api/package.json apps/api/package.json +RUN bun install --frozen-lockfile + +COPY packages/shared packages/shared +COPY apps/web apps/web +RUN bun run --cwd packages/shared build +RUN bun run --cwd apps/web build + +FROM nginx:1.27-alpine AS runner +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/apps/web/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..dc8f2f6 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass https://api.zeavisedu.asepharyana.tech/api/; + proxy_set_header Host api.zeavisedu.asepharyana.tech; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/bun.lock b/bun.lock old mode 100755 new mode 100644 index 33511f9..00ebf72 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { - "lockfileVersion": 0, + "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "devDependencies": { @@ -12,7 +13,7 @@ "version": "0.1.0", "dependencies": { "@elysiajs/cors": "1.4.2", - "@zeavis/shared": "packages/shared", + "@zeavis/shared": "workspace:*", "bcryptjs": "^3.0.3", "drizzle-orm": "^0.45.2", "elysia": "^1.4.28", @@ -32,7 +33,7 @@ "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-query": "^5.100.11", "@vitejs/plugin-react": "^6.0.2", - "@zeavis/shared": "packages/shared", + "@zeavis/shared": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.16.0", @@ -250,11 +251,11 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], - "@zeavis/api": ["@zeavis/api@workspace:apps/api", { "dependencies": { "@elysiajs/cors": "1.4.2", "@zeavis/shared": "packages/shared", "bcryptjs": "^3.0.3", "drizzle-orm": "^0.45.2", "elysia": "^1.4.28", "postgres": "^3.4.9" }, "devDependencies": { "@types/bcryptjs": "^3.0.0", "@types/bun": "latest", "drizzle-kit": "^0.31.10", "typescript": "^6.0.3" } }], + "@zeavis/api": ["@zeavis/api@workspace:apps/api"], - "@zeavis/shared": ["@zeavis/shared@workspace:packages/shared", { "devDependencies": { "typescript": "^6.0.3" } }], + "@zeavis/shared": ["@zeavis/shared@workspace:packages/shared"], - "@zeavis/web": ["@zeavis/web@workspace:apps/web", { "dependencies": { "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-query": "^5.100.11", "@vitejs/plugin-react": "^6.0.2", "@zeavis/shared": "packages/shared", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", "tailwind-merge": "^3.6.0", "zustand": "^5.0.13" }, "devDependencies": { "@tailwindcss/postcss": "^4.3.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "postcss": "^8.5.15", "typescript": "^6.0.3", "vite": "^8.0.14" } }], + "@zeavis/web": ["@zeavis/web@workspace:apps/web"], "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7232798 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,61 @@ +networks: + app-shared-net: + external: true + name: app-shared-net + +services: + web: + image: ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/web:main + container_name: zeavis-web + restart: always + networks: + - app-shared-net + env_file: + - .env + labels: + traefik.enable: "true" + traefik.http.routers.zeavis-web.rule: Host(`zeavisedu.asepharyana.tech`) + traefik.http.routers.zeavis-web.entrypoints: websecure + traefik.http.routers.zeavis-web.tls: "true" + traefik.http.routers.zeavis-web.tls.certresolver: letsencrypt + traefik.http.services.zeavis-web.loadbalancer.server.port: "80" + + api: + image: ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/api:main + container_name: zeavis-api + restart: always + networks: + - app-shared-net + env_file: + - .env + environment: + NODE_ENV: production + API_PORT: "3000" + WEB_APP_URL: https://zeavisedu.asepharyana.tech + ML_SERVICE_URL: https://ml.zeavisedu.asepharyana.tech + labels: + traefik.enable: "true" + traefik.http.routers.zeavis-api.rule: Host(`api.zeavisedu.asepharyana.tech`) + traefik.http.routers.zeavis-api.entrypoints: websecure + traefik.http.routers.zeavis-api.tls: "true" + traefik.http.routers.zeavis-api.tls.certresolver: letsencrypt + traefik.http.services.zeavis-api.loadbalancer.server.port: "3000" + + ml: + image: ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/ml:main + container_name: zeavis-ml + restart: always + networks: + - app-shared-net + env_file: + - .env + environment: + MODEL_PATH: /app/model/best_model.keras + MODEL_INPUT_SIZE: "224" + labels: + traefik.enable: "true" + traefik.http.routers.zeavis-ml.rule: Host(`ml.zeavisedu.asepharyana.tech`) + traefik.http.routers.zeavis-ml.entrypoints: websecure + traefik.http.routers.zeavis-ml.tls: "true" + traefik.http.routers.zeavis-ml.tls.certresolver: letsencrypt + traefik.http.services.zeavis-ml.loadbalancer.server.port: "8000" diff --git a/docs/superpowers/plans/2026-05-22-docker-deployment.md b/docs/superpowers/plans/2026-05-22-docker-deployment.md index cfd27f9..87344ae 100644 --- a/docs/superpowers/plans/2026-05-22-docker-deployment.md +++ b/docs/superpowers/plans/2026-05-22-docker-deployment.md @@ -37,7 +37,6 @@ Write this exact file: .moon/cache node_modules **/node_modules -bun.lock .env .env.* !.env.example @@ -47,7 +46,8 @@ Machine_Learning/dataset Machine_Learning/dataset.zip Machine_Learning/dataset_*.zip Machine_Learning/model -Machine_Learning/best_model +Machine_Learning/best_model/* +!Machine_Learning/best_model/best_model.keras apps/web/dist apps/api/dist __pycache__ @@ -55,6 +55,8 @@ __pycache__ .pytest_cache .venv venv +**/.venv +**/venv ``` - [ ] **Step 2: Verify ignore file exists**