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.