diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e621c68 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.gitea +.DS_Store +.idea +.vscode diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..8240cd6 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build Docker Image + +on: + push: + tags: + - "v*" + +jobs: + build-frontend: + runs-on: docker + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Build & push frontend image + run: | + set -e + + IMAGE="git.koppkb.com/kopkb/my-kopkb-fe" + TAGS="-t ${IMAGE}:${{ gitea.sha }}" + + if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then + TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}" + fi + + echo "Building and pushing frontend: ${TAGS}" + docker buildx build \ + --platform linux/amd64 \ + --cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \ + --cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \ + -f docker/Dockerfile \ + --build-arg VITE_API_BASE_URL=https://api.koppkb.com \ + --build-arg VITE_APP_URL=https://mykopkb.koppkb.com \ + ${TAGS} \ + --push . diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..35d91a4 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,106 @@ +name: Deploy to Production + +# Manual production deploy — pulls the pre-built frontend image from the registry +on: + workflow_dispatch: + inputs: + image_tag: + description: "Docker image tag to deploy (commit SHA or version tag, e.g. v1.0.0)" + required: true + default: "latest" + type: string + +env: + FRONTEND_IMAGE: git.koppkb.com/kopkb/my-kopkb-fe + DEPLOY_DIR: /home/arrahn/Project + +jobs: + deploy: + runs-on: host + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.image_tag }} + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Verify image exists in registry + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + IMAGE="${FRONTEND_IMAGE}:${IMAGE_TAG}" + + echo "Checking if image exists: ${IMAGE}" + if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then + echo "ERROR: Image ${IMAGE} does not exist in registry!" + exit 1 + fi + echo "✓ ${IMAGE} found" + + - name: Pull Docker image + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + docker pull "${FRONTEND_IMAGE}:${IMAGE_TAG}" + echo "✓ Image pulled successfully" + + - name: Sync compose file and deploy + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + + mkdir -p "${DEPLOY_DIR}" + cp docker/docker-compose.production.yml "${DEPLOY_DIR}/docker-compose.frontend.yml" + + docker network inspect mykopkb-production-network >/dev/null 2>&1 \ + || docker network create mykopkb-production-network + + cd "${DEPLOY_DIR}" + + export IMAGE_TAG + + docker rm -f mykopkb-fe-production || true + docker compose -f docker-compose.frontend.yml -p mykopkb-fe pull + docker compose -f docker-compose.frontend.yml -p mykopkb-fe up -d + + echo "✓ Deployed frontend IMAGE_TAG=${IMAGE_TAG}" + + - name: Verify deployment + run: | + set -e + cd "${DEPLOY_DIR}" + + echo "Waiting for the frontend to start..." + sleep 10 + + STATUS=$(docker compose -f docker-compose.frontend.yml -p mykopkb-fe ps --status running --format '{{.Name}}' frontend 2>/dev/null || true) + if [ -z "$STATUS" ]; then + echo "ERROR: frontend is not running" + docker compose -f docker-compose.frontend.yml -p mykopkb-fe ps + docker compose -f docker-compose.frontend.yml -p mykopkb-fe logs --tail=50 frontend || true + exit 1 + fi + echo "✓ frontend is running (${STATUS})" + + FE_PORT=$(grep -E '^FE_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + FE_PORT=${FE_PORT:-8081} + + for i in 1 2 3 4 5 6; do + if curl -sf "http://127.0.0.1:${FE_PORT}/health" > /dev/null; then + echo "✓ frontend health check passed (http://127.0.0.1:${FE_PORT}/health)" + exit 0 + fi + echo "Waiting for frontend... attempt ${i}/6" + sleep 5 + done + + echo "ERROR: frontend health check failed (http://127.0.0.1:${FE_PORT}/health)" + docker compose -f docker-compose.frontend.yml -p mykopkb-fe logs --tail=50 frontend || true + exit 1 diff --git a/docker/Dockerfile b/docker/Dockerfile index d651ba4..ae34a8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,12 @@ # Stage 1: Build Vue SPA FROM node:22 AS builder WORKDIR /app -COPY fe/package*.json fe/pnpm-lock.yaml fe/.npmrc fe/pnpm-workspace.yaml ./ +COPY package*.json pnpm-lock.yaml .npmrc pnpm-workspace.yaml ./ # Skip prepare (cypress install); rebuild esbuild only — required for Vite RUN npm install -g pnpm && \ pnpm install --frozen-lockfile --ignore-scripts && \ pnpm rebuild esbuild -COPY fe/ ./ +COPY . ./ ARG VITE_APP_NAME="MyKOPKB" ARG VITE_APP_VERSION="1.0" ARG VITE_API_BASE_URL=https://api.koppkb.com @@ -22,6 +22,6 @@ RUN pnpm exec vite build --mode=production # Stage 2: Serve static files FROM nginx:1.27-alpine -COPY fe/docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/docker/docker-compose.production.yml b/docker/docker-compose.production.yml new file mode 100644 index 0000000..680ca2f --- /dev/null +++ b/docker/docker-compose.production.yml @@ -0,0 +1,25 @@ +services: + frontend: + image: git.koppkb.com/kopkb/my-kopkb-fe:${IMAGE_TAG:-latest} + container_name: mykopkb-fe-production + restart: unless-stopped + ports: + - "${FE_PORT:-8081}:80" + networks: + - mykopkb-production-network + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + mykopkb-production-network: + name: mykopkb-production-network + external: true