feat: add Docker deployment workflow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Asep Haryana Saputra
2026-05-22 21:20:56 +00:00
co-authored by Claude Opus 4.7
parent 5ca95907a0
commit 1978597bcf
9 changed files with 273 additions and 8 deletions
+26
View File
@@ -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
+98
View File
@@ -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
+21
View File
@@ -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"]
+19
View File
@@ -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"]
+19
View File
@@ -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;"]
+18
View File
@@ -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;
}
}
Executable → Regular
+7 -6
View File
@@ -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=="],
+61
View File
@@ -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"
@@ -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**