Home / GitLab CI/CD
Enterprise DevSecOps • Java Pipelines

Advanced Java WebApp CI/CD with GitLab Runner & Automated Rollbacks 🚀

A production blueprint for engineering resilient Java Spring Boot pipelines: custom Docker runner registration, distributed Maven caching, Trivy container security gating, zero-downtime Kubernetes deployments, and automated health-check rollbacks.

Published by AHP • cicdclouds.com

1. Production Pipeline Philosophy: Fail Fast, Recover Faster

In mission-critical enterprise environments, deploying Java microservices demands more than simply executing mvn clean package and pushing a container to a registry. A production-grade CI/CD pipeline must integrate automated quality gates, security compliance scans, deterministic artifact versioning, and most importantly, an automated rollback loop.

If a newly deployed container passes unit tests but fails application-level health checks or database handshakes in Kubernetes, the pipeline must automatically detect the failure and revert the deployment immediately without requiring manual operator intervention.

2. Configuring Dedicated Docker GitLab Runners

While shared GitLab SaaS runners are convenient for small projects, enterprise Java pipelines run into two severe bottlenecks on shared infrastructure:

  • Slow Compilation Cycles: Cold Maven repository caches force the runner to download hundreds of megabytes of dependencies on every pipeline execution.
  • Network Boundary Constraints: Shared runners cannot access private VPC subnets, internal Kubernetes clusters, or proprietary artifact repositories without complex proxy tunnels.

Provisioning a dedicated runner on an internal EC2 instance or virtual machine resolves these limitations. Execute the non-interactive registration command below:

# Register a dedicated Docker-executor GitLab Runner
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "$GITLAB_RUNNER_REGISTRATION_TOKEN" \
  --executor "docker" \
  --docker-image "maven:3.9.9-eclipse-temurin-21" \
  --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
  --docker-volumes "/cache/maven:/root/.m2/repository" \
  --description "cicdclouds-prod-java-runner" \
  --tag-list "java,docker,k8s,production"

Mounting the local directory /cache/maven into /root/.m2/repository ensures that third-party Java libraries persist across runner jobs, cutting compile times from minutes to seconds.

3. Production .gitlab-ci.yml with Automated Rollback Engine

The pipeline below implements a multi-stage lifecycle: Build → Test → Containerize → Security Scan → Deploy → Post-Deploy Verification & Automated Rollback.

stages:
  - build
  - test
  - package
  - security-scan
  - deploy
  - verify-rollback

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
  APP_NAME: "java-webapp"
  NAMESPACE: "production"

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .m2/repository
    - target/*.jar

# 1. Compile Source Code
compile-job:
  stage: build
  image: maven:3.9.9-eclipse-temurin-21
  script:
    - mvn clean compile -B

# 2. Execute Automated Unit Tests & Publish Test Reports
unit-tests:
  stage: test
  image: maven:3.9.9-eclipse-temurin-21
  script:
    - mvn test -B
    - mvn package -DskipTests=true
  artifacts:
    when: always
    paths:
      - target/*.jar
    reports:
      junit:
        - target/surefire-reports/TEST-*.xml

# 3. Build Immutable Container Image
docker-package:
  stage: package
  image: docker:27.2.0
  services:
    - docker:27.2.0-dind
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build --build-arg JAR_FILE=target/*.jar -t "$IMAGE_TAG" .
    - docker push "$IMAGE_TAG"
  only:
    - main

# 4. DevSecOps: Vulnerability Assessment with Trivy
vulnerability-scan:
  stage: security-scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL --no-progress "$IMAGE_TAG"
  allow_failure: false
  only:
    - main

# 5. Kubernetes Rolling Deployment
deploy-to-cluster:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
    - kubectl config set-credentials admin --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=admin
    - kubectl config use-context default
    # Apply rolling image update
    - kubectl set image deployment/$APP_NAME $APP_NAME=$IMAGE_TAG -n $NAMESPACE
    # Wait up to 120s for pods to become ready
    - kubectl rollout status deployment/$APP_NAME -n $NAMESPACE --timeout=120s
  only:
    - main

# 6. Post-Deployment Synthetic Health Check & Automated Rollback
healthcheck-and-rollback:
  stage: verify-rollback
  image: bitnami/kubectl:latest
  script:
    - kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
    - kubectl config set-credentials admin --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=admin
    - kubectl config use-context default
    - echo "Executing post-deployment synthetic verification..."
    # Probe application endpoint 5 times
    - |
      for i in {1..5}; do
        STATUS=$(kubectl run curl-test --rm --restart=Never -i --image=curlimages/curl -- \
          curl -s -o /dev/null -w "%{http_code}" http://$APP_NAME.$NAMESPACE.svc.cluster.local:8080/actuator/health)
        if [ "$STATUS" -eq 200 ]; then
          echo "Health Check Attempt $i Passed (HTTP 200)"
          sleep 3
        else
          echo "ALERT: Synthetic Health Check Failed with HTTP status: $STATUS"
          echo "Triggering immediate automated rollback to previous deployment revision..."
          kubectl rollout undo deployment/$APP_NAME -n $NAMESPACE
          kubectl rollout status deployment/$APP_NAME -n $NAMESPACE
          exit 1
        fi
      done
      echo "Deployment verification completed successfully. Release validated!"
  only:
    - main

4. Key Architectural Mechanisms Explained

1. Fast-Feedback Test Telemetry

By declaring reports: junit: target/surefire-reports/TEST-*.xml, GitLab CI parses standard JUnit test results directly into the merge request UI. Engineers see test failures, stack traces, and execution times without digging through raw console logs.

2. Shift-Left Security Gating with Trivy

The vulnerability-scan stage scans the compiled container layer for known Common Vulnerabilities and Exposures (CVEs). Setting --exit-code 1 --severity CRITICAL halts the deployment pipeline immediately if unpatched operating system vulnerabilities are detected in the base image.

3. Zero-Downtime Rollout & Fallback Loop

The deployment uses Kubernetes rolling updates coupled with a 120-second timeout. If pods get stuck in CrashLoopBackOff or fail liveness probes, kubectl rollout status exits with an error code, preventing traffic routing to unready replicas.

The subsequent healthcheck-and-rollback step acts as a final synthetic safety net: it queries the Spring Boot Actuator endpoint (/actuator/health) inside the cluster network. If the response returns non-200 status codes, it automatically executes kubectl rollout undo to roll back the cluster to the last stable replica set.

5. Common GitLab CI/CD Pitfalls & Troubleshooting

Managing automated CI/CD pipelines across enterprise environments presents common failure modes:

1. Docker-in-Docker (dind) Permission Failures

When executing docker build inside containerized runners, jobs frequently fail with Cannot connect to the Docker daemon at unix:///var/run/docker.sock.

Remedy: In your runner host configuration (/etc/gitlab-runner/config.toml), set privileged = true in the [runners.docker] section, or bind-mount /var/run/docker.sock directly into the build container.

2. Inadvertent Cache Invalidation

If the cache key is set to $CI_JOB_NAME, the Maven cache is wiped on every new git branch.

Remedy: Use $CI_COMMIT_REF_SLUG to share caches across builds within the same branch, or use a fixed key like maven-global-cache to share dependencies across all branches.

3. Rollout Undo Hangs Indefinitely

If the previous deployment revision itself was in an unready state, executing kubectl rollout undo will fail to resolve the outage.

Remedy: Always enforce GitOps hygiene by tagging git commits with semantic versions, and retain revision history by specifying revisionHistoryLimit: 10 in your Kubernetes deployment manifests.