From 523218faa589de45eae17b08413f32fb0073504f Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Thu, 23 Jul 2026 11:31:17 +0800 Subject: [PATCH] DONE: pipeline cicd for backend --- .gitea/workflows/be-deploy.yml | 112 ++++++++++++++++++++++++++ .gitea/workflows/build.yml | 80 ++++++++++++++++++ be/.dockerignore | 10 +++ be/.env.example | 12 +++ be/.env.production | 12 +++ be/.gitignore | 3 + be/Dockerfile | 29 ++++++- be/README.md | 41 ++++++++-- be/docker-compose.yml | 48 +++++++++-- be/src/main/resources/application.yml | 17 ++-- 10 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/be-deploy.yml create mode 100644 .gitea/workflows/build.yml create mode 100644 be/.dockerignore create mode 100644 be/.env.example create mode 100644 be/.env.production diff --git a/.gitea/workflows/be-deploy.yml b/.gitea/workflows/be-deploy.yml new file mode 100644 index 0000000..fb5d489 --- /dev/null +++ b/.gitea/workflows/be-deploy.yml @@ -0,0 +1,112 @@ +name: Deploy Backend to Production + +# Manual production deploy — pulls pre-built backend 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 + type: string + +env: + BACKEND_IMAGE: git.koppkb.com/kopkb/qms-be + DEPLOY_DIR: /home/arrahn/Project/qms + +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 backend image exists in registry + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + IMAGE="${BACKEND_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 backend image + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + docker pull "${BACKEND_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 be/docker-compose.yml "${DEPLOY_DIR}/docker-compose.yml" + + cd "${DEPLOY_DIR}" + + if [ ! -f .env ]; then + echo "ERROR: ${DEPLOY_DIR}/.env is missing on the server" + echo "Create it from be/.env.example (or be/.env.production) before deploying." + exit 1 + fi + + export IMAGE_TAG + export BACKEND_IMAGE + + docker compose -f docker-compose.yml pull backend + docker compose -f docker-compose.yml up -d --remove-orphans + + echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}" + + - name: Verify deployment + run: | + set -e + cd "${DEPLOY_DIR}" + + echo "Waiting for services to start..." + sleep 15 + + for SERVICE in mysql backend; do + STATUS=$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' "$SERVICE" 2>/dev/null || true) + if [ -z "$STATUS" ]; then + echo "ERROR: ${SERVICE} is not running" + docker compose -f docker-compose.yml ps + docker compose -f docker-compose.yml logs --tail=50 "$SERVICE" || true + exit 1 + fi + echo "✓ ${SERVICE} is running (${STATUS})" + done + + BACKEND_HOST_PORT=$(grep -E '^BACKEND_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + BACKEND_HOST_PORT=${BACKEND_HOST_PORT:-8080} + + # No actuator yet — treat any HTTP response as "server is up" + for i in 1 2 3 4 5 6 7 8; do + CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${BACKEND_HOST_PORT}/" || echo "000") + if [ "${CODE}" != "000" ]; then + echo "✓ Backend is responding on port ${BACKEND_HOST_PORT} (HTTP ${CODE})" + exit 0 + fi + echo "Waiting for backend... attempt ${i}/8" + sleep 5 + done + + echo "ERROR: Backend is not responding on port ${BACKEND_HOST_PORT}" + docker compose -f docker-compose.yml logs --tail=50 backend || true + exit 1 diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..b14895a --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,80 @@ +name: Build Docker Image + +on: + push: + branches: + - main + tags: + - "v*" + +jobs: + build-backend: + 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 backend image + run: | + set -e + + IMAGE="git.koppkb.com/kopkb/qms-be" + 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 backend: ${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 be/Dockerfile \ + ${TAGS} \ + --push \ + be + + # --- Frontend builds (enable when FE Dockerfiles are ready) --- + # 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/qms-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 fe/docker/Dockerfile \ + # --build-arg VITE_API_BASE_URL=https://api.example.com \ + # ${TAGS} \ + # --push . diff --git a/be/.dockerignore b/be/.dockerignore new file mode 100644 index 0000000..48b65d8 --- /dev/null +++ b/be/.dockerignore @@ -0,0 +1,10 @@ +target/ +uploads/ +.git/ +.gitignore +.idea/ +*.iml +.vscode/ +*.md +.env +.DS_Store diff --git a/be/.env.example b/be/.env.example new file mode 100644 index 0000000..6b24fa6 --- /dev/null +++ b/be/.env.example @@ -0,0 +1,12 @@ +# Host ports (change these if they collide on your single server) +BACKEND_HOST_PORT=8080 +MYSQL_HOST_PORT=3306 + +# MySQL +MYSQL_ROOT_PASSWORD=password +MYSQL_DATABASE=qms + +# App +JWT_SECRET_KEY=change-me-to-a-long-random-secret +GOOGLE_CLIENT_ID=dummy-google-client-id +NOTIFICATIONS_MOCK=true diff --git a/be/.env.production b/be/.env.production new file mode 100644 index 0000000..d872f50 --- /dev/null +++ b/be/.env.production @@ -0,0 +1,12 @@ +# Host ports (change these if they collide on your single server) +BACKEND_HOST_PORT=8080 +MYSQL_HOST_PORT=3306 + +# MySQL +MYSQL_ROOT_PASSWORD=R8qjjBJDg9NtL2IwcnoFMkn8pktwAZ;wsl +MYSQL_DATABASE=qms + +# App +JWT_SECRET_KEY=a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF +GOOGLE_CLIENT_ID=dummy-google-client-id +NOTIFICATIONS_MOCK=true diff --git a/be/.gitignore b/be/.gitignore index f582c73..ffae87a 100644 --- a/be/.gitignore +++ b/be/.gitignore @@ -34,3 +34,6 @@ build/ ### Uploads ### uploads/ + +### Docker / env ### +.env diff --git a/be/Dockerfile b/be/Dockerfile index fdc32fd..2f2e3b1 100644 --- a/be/Dockerfile +++ b/be/Dockerfile @@ -1,3 +1,26 @@ -FROM openjdk:17-jdk-alpine -COPY target/bbqms-0.0.1-SNAPSHOT.jar ./app.jar -ENTRYPOINT ["java", "-jar", "/app.jar"] +# Build stage +FROM maven:3.9-eclipse-temurin-17-alpine AS build +WORKDIR /app + +COPY pom.xml . +COPY mvnw . +COPY .mvn .mvn +COPY src ./src + +RUN ./mvnw -q -DskipTests package + +# Runtime stage +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app + +RUN addgroup -S qms && adduser -S qms -G qms \ + && mkdir -p /app/uploads/ads \ + && chown -R qms:qms /app + +COPY --from=build /app/target/bbqms-0.0.1-SNAPSHOT.jar app.jar + +USER qms + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/be/README.md b/be/README.md index 199c7f9..6d4b865 100644 --- a/be/README.md +++ b/be/README.md @@ -1,9 +1,38 @@ -## Build instructions +## Build instructions (local) -- Enter your database details in the **src/main/resources/application.yml** file -- Through IntelliJ simply click on the run button -- Or through console - - ./mvnw dependency:resolve - - ./mvnw spring-boot:run +- Enter your database details in the **src/main/resources/application.yml** file (or use env vars) +- Through IntelliJ simply click on the run button +- Or through console + - `./mvnw dependency:resolve` + - `./mvnw spring-boot:run` ##### **NOTE:** requires Java 17 and MySQL 8 + +## Docker deployment (single server) + +From the `be/` directory: + +```bash +cp .env.example .env +# Edit .env if ports 8080 / 3306 are already in use on the server + +docker compose up -d --build +``` + +Services and default host ports: + +| Service | Container | Host port (configurable) | +|----------|-----------|---------------------------| +| Backend | qms-backend | `BACKEND_HOST_PORT` → **8080** | +| MySQL | qms-mysql | `MYSQL_HOST_PORT` → **3306** | + +API base URL on the server: `http://:8080` + +Useful commands: + +```bash +docker compose ps +docker compose logs -f backend +docker compose down # stop containers (keeps DB volume) +docker compose down -v # stop and delete DB/uploads volumes +``` diff --git a/be/docker-compose.yml b/be/docker-compose.yml index 6dabeb7..ca1e68d 100644 --- a/be/docker-compose.yml +++ b/be/docker-compose.yml @@ -2,18 +2,56 @@ services: mysql: image: mysql:8 container_name: qms-mysql + restart: unless-stopped ports: - - "3306:3306" + # Host port can be changed via MYSQL_HOST_PORT in .env (default 3306) + - "${MYSQL_HOST_PORT:-3306}:3306" environment: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: qms + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password} + MYSQL_DATABASE: ${MYSQL_DATABASE:-qms} volumes: - qms-mysql-data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppassword"] + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD:-password}"] interval: 5s timeout: 5s - retries: 10 + retries: 20 + start_period: 20s + networks: + - qms-net + + backend: + image: ${BACKEND_IMAGE:-qms-backend}:${IMAGE_TAG:-local} + build: + context: . + dockerfile: Dockerfile + container_name: qms-backend + restart: unless-stopped + ports: + # Host port can be changed via BACKEND_HOST_PORT in .env (default 8080) + - "${BACKEND_HOST_PORT:-8080}:8080" + environment: + SERVER_PORT: 8080 + # Use Docker service name "mysql", not localhost + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-qms}?allowPublicKeyRetrieval=true&useSSL=false + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF} + ADS_UPLOAD_DIR: /app/uploads/ads + NOTIFICATIONS_MOCK: ${NOTIFICATIONS_MOCK:-true} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-dummy-google-client-id} + volumes: + - qms-uploads:/app/uploads + depends_on: + mysql: + condition: service_healthy + networks: + - qms-net volumes: qms-mysql-data: + qms-uploads: + +networks: + qms-net: + driver: bridge diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index 701708b..53ae3ee 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -1,3 +1,6 @@ +server: + port: ${SERVER_PORT:8080} + spring: application: name: qms @@ -6,9 +9,9 @@ spring: hibernate: ddl-auto: none datasource: - url: jdbc:mysql://localhost:3306/qms - username: root - password: password + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/qms} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} servlet: multipart: max-file-size: 100MB @@ -18,14 +21,14 @@ spring: client: registration: google: - client-id: dummy-google-client-id + client-id: ${GOOGLE_CLIENT_ID:dummy-google-client-id} flyway: schemas: qms jwt: header-title: Authorization token-prefix: Bearer - secret-key: a68uiaDQ0V3iLjF4DqMuS13GAVwkut55dlFbGCLyXTF + secret-key: ${JWT_SECRET_KEY} authorities-key: USER_AUTHORITIES token-validity-time: PT30M tfa: @@ -35,6 +38,6 @@ tenancy: default-code: DFLT notifications: expo-url: https://exp.host/--/api/v2/push/send - mock: true + mock: ${NOTIFICATIONS_MOCK:true} ads: - upload-dir: uploads/ads + upload-dir: ${ADS_UPLOAD_DIR:uploads/ads}