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..a70dde8 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,98 @@ +name: Deploy to Production + +# Manual production deploy — pulls pre-built images 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 + +jobs: + deploy: + runs-on: mykopkb-production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.image_tag }} + + - name: Verify image exists in registry + run: | + set -e + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + IMAGE="${FRONTEND_IMAGE}:${{ inputs.image_tag }}" + echo "Checking if image exists: ${IMAGE}" + docker manifest inspect "${IMAGE}" > /dev/null + echo "✓ ${IMAGE} found" + + - name: Deploy over SSH + env: + PROD_HOST: ${{ secrets.PROD_HOST }} + PROD_USER: ${{ secrets.PROD_USER }} + PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }} + PROD_DEPLOY_DIR: ${{ secrets.PROD_DEPLOY_DIR }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + IMAGE_TAG: ${{ inputs.image_tag }} + FRONTEND_IMAGE: git.koppkb.com/kopkb/my-kopkb-fe + run: | + set -euo pipefail + + if ! command -v ssh >/dev/null 2>&1; then + apt-get update && apt-get install -y openssh-client + fi + + mkdir -p ~/.ssh + printf '%s\n' "$PROD_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "$PROD_HOST" >> ~/.ssh/known_hosts + + SSH=(ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes "${PROD_USER}@${PROD_HOST}") + + "${SSH[@]}" "mkdir -p '${PROD_DEPLOY_DIR}'" + scp -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes \ + docker/docker-compose.production.yml \ + "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_DIR}/docker-compose.frontend.yml" + + "${SSH[@]}" "bash -s" << EOF + set -euo pipefail + echo "${REGISTRY_PASSWORD}" | docker login git.koppkb.com \ + -u "${REGISTRY_USERNAME}" --password-stdin + + IMAGE="${FRONTEND_IMAGE}:${IMAGE_TAG}" + echo "Checking if image exists: ${IMAGE}" + docker manifest inspect "${IMAGE}" > /dev/null + echo "✓ ${IMAGE} found" + + echo "Deploying frontend IMAGE=${IMAGE}" + + docker network inspect mykopkb-production-network >/dev/null 2>&1 \ + || docker network create mykopkb-production-network + + cd "${PROD_DEPLOY_DIR}" + export IMAGE_TAG="${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 + + sleep 10 + STATUS=\$(docker compose -f docker-compose.frontend.yml -p mykopkb-fe ps --status running --format '{{.Name}}' frontend || true) + if [ -z "\$STATUS" ]; then + docker compose -f docker-compose.frontend.yml -p mykopkb-fe logs --tail=50 frontend || true + exit 1 + fi + + FE_PORT=\$(grep -E '^FE_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + FE_PORT=\${FE_PORT:-8081} + curl -sf "http://127.0.0.1:\${FE_PORT}/health" + echo "✓ Deployed frontend IMAGE_TAG=${IMAGE_TAG}" + EOF 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