A modular, multi-environment GitLab CI/CD pipeline specifically designed for Java application deployment with automated rollback capabilities.
- Overview
- Quick Start Guide
- Pipeline Structure
- Environment Support
- Features
- Rollback Mechanism
- SSH Authentication
- Java Application Support
- Multi-Server Deployment
- Auto-Promotion Prevention
- Testing Framework
- Deployment Process
This repository contains a GitLab CI/CD configuration specifically designed for deploying Java applications to multiple environments (test, staging, production) with comprehensive deployment, rollback, and notification capabilities. The pipeline is designed to be modular, maintainable, and follows GitLab CI/CD best practices for Java applications deployed as systemd services.
This section will help you quickly set up the GitLab CI/CD pipeline for your Java application.
-
Copy the Files: Copy the
.gitlab-ci.yml
andci/
directory to your Java project root. -
Update Essential Variables: Edit
ci/variables.yml
and update these key variables:variables: # Application name (used for deployment directories and service names) APP_NAME: "your-java-app" # Deployment server hostname or IP address DEPLOY_HOST: "your-server.example.com" # User account on deployment servers APP_USER: "your-app-user" # Email for notifications NOTIFICATION_EMAIL: "your-team@example.com"
-
Commit and Push: Commit these changes and push to your GitLab repository.
-
Run the Pipeline: Go to CI/CD > Pipelines in GitLab and run your first pipeline.
That's it! Your Java application will be built and deployed using the pipeline.
The internal Maven Wrapper is configured to use a local repository within the project:
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
The pipeline uses an internal Maven Wrapper component for consistent builds. Make sure the Maven Wrapper script has the correct permissions in your repository:
git update-index --chmod=+x mvnw
Default (skip tests):
BUILD_COMMAND: "$CI_PROJECT_DIR/mvnw package -DskipTests"
With tests:
BUILD_COMMAND: "$CI_PROJECT_DIR/mvnw package"
With specific profile:
BUILD_COMMAND: "$CI_PROJECT_DIR/mvnw package -P production -DskipTests"
Default:
RUNTIME_OPTS: "-Xmx512m -Xms256m"
For larger applications:
RUNTIME_OPTS: "-Xmx2g -Xms1g -XX:+UseG1GC"
With system properties:
RUNTIME_OPTS: "-Xmx512m -Dspring.profiles.active=${CI_ENVIRONMENT_NAME} -Dserver.port=8080"
# In .gitlab-ci.yml, update the build job:
build:
extends: .build_template
image: $BUILDER_IMAGE_JAVA_21 # Reference your internal builder image
The CI/CD pipeline is organized into the following stages:
Stage | Purpose | Key Features |
---|---|---|
Validate | Ensures deployments only occur from protected branches | • Branch protection rules • Auto-promotion prevention • Environment validation |
Build | Compiles and packages the Java application | • Maven/Gradle support • Artifact collection • Dependency caching |
Deploy | Deploys the application to the target environment | • Multi-server support • Systemd service creation • Health checks |
Notify | Sends notifications about deployment status | • Email notifications • Notification Service integration • Detailed status reports |
Rollback | Handles automatic and manual rollbacks if needed | • Automatic failure detection • Version history management • One-click manual rollback |
The diagram above illustrates the Java application deployment pipeline stages and their relationships.
The diagram above illustrates the Java application deployment process, from preparation through validation. It shows the step-by-step flow of how Java artifacts are deployed to target environments.
.
├── .gitlab-ci.yml # Main CI/CD configuration file
└── ci/ # Modular CI components
├── variables.yml # Global and environment-specific variables
├── functions.yml # Reusable shell functions
├── build.yml # Build job templates
├── deploy.yml # Deployment job templates
├── rollback.yml # Rollback job templates
└── notify.yml # Notification job templates
The pipeline supports three environments with multi-server deployment capabilities:
The diagram above shows how Git branches map to deployment environments with appropriate controls. It illustrates the flow from development through test, staging, and production environments.
-
Test
- Single server deployment:
test-server.example.com
- Manual deployments from
develop
branch and feature branches - Customised for testing purposes
- Auto-promotion prevention enabled
- Single server deployment:
-
Staging
- Multi-server deployment:
staging-server-1.example.com
,staging-server-2.example.com
- Manual deployments from
develop
orrelease/*
branches - Uses
staging-appuser
for deployment - Auto-promotion prevention enabled
- Multi-server deployment:
-
Production
- Multi-server deployment:
production-server-1.example.com
,production-server-2.example.com
- Manual deployments from
main
,master
, orproduction
branches - Uses
prod-appuser
for deployment - Notification Service enabled
- Auto-promotion prevention enabled
- Multi-server deployment:
- Modular Design: Pipeline components are separated into reusable, maintainable files
- Java-Specific Configuration: Optimized for Java applications with Maven/Gradle support
- Multi-Environment Support: Configured for test, staging, and production environments
- Multi-Server Deployment: Deploy to multiple servers within an environment
- Auto-Promotion Prevention: Ensure deployments are manually triggered between environments
- Robust Error Handling: Comprehensive logging and error recovery
- Test Mode: Simulate pipeline execution without making actual changes
- Automated Rollback: Recover from failed deployments automatically
- Manual Rollbacks: On-demand rollback capability for each environment
- Deployment Retention: Configurable number of deployments to retain
- Health Checks: Validates deployment success with configurable retries
- Notifications: Flexible notification system supporting email and Notification Service
- Detailed Logging: Comprehensive logging with timestamps and log levels
The pipeline includes both automatic and manual rollback capabilities:
The diagram above illustrates the automatic and manual rollback strategies with comprehensive testing. It shows how the system detects deployment failures and initiates rollbacks to maintain service availability.
- Automatic Rollback: Triggered when deployment health checks fail
- Manual Rollback: Can be triggered manually for any environment
Rollbacks follow a consistent process:
- Identify the appropriate rollback target (last successful deployment or latest backup)
- Stop the current service
- Update the symlink to point to the rollback target
- Start the service
- Perform health checks to verify the rollback was successful
The pipeline uses sshpass
for SSH authentication. All SSH commands are wrapped in functions that handle authentication consistently:
function ssh_cmd() {
sshpass $sshpass ssh ${APP_USER}@$DEPLOY_HOST "$@"
}
This pipeline is specifically designed for Java applications deployed as systemd services:
- Java-Optimized: Configured for Java applications with JVM options and classpath handling
- Maven/Gradle Support: Works with both Maven and Gradle build systems
- JAR Deployment: Handles JAR file deployment with proper versioning
- JVM Configuration: Configurable JVM options for different environments
- Systemd Integration: Creates and manages systemd services for Java applications
variables:
# Application details
APP_NAME: "my-java-app"
APP_VERSION: "1.0.0"
# Build configuration (Maven)
BUILD_COMMAND: "./mvnw clean package -DskipTests"
# Artifact settings
ARTIFACT_PATH: "target/my-java-app-1.0.0.jar"
ARTIFACT_NAME: "my-java-app-1.0.0.jar"
# Java runtime configuration
RUNTIME_PATH: "/usr/bin/java"
RUNTIME_OPTS: "-Xmx512m -Dspring.profiles.active=${CI_ENVIRONMENT_NAME}"
# Service configuration
START_COMMAND: "/usr/bin/java -Xmx512m -jar /opt/app/current/my-java-app-1.0.0.jar"
WORKING_DIRECTORY: "/opt/app/current"
SERVICE_ENV_VARS: "JAVA_HOME=/usr/lib/jvm/java-17-openjdk,SPRING_PROFILES_ACTIVE=${CI_ENVIRONMENT_NAME}"
The deployment process is optimized for Java applications:
The diagram above shows the detailed steps in the Java application deployment process.
- Build: Executes Maven or Gradle to build the Java application
- Artifact Collection: Collects the JAR file(s) from the target directory
- Upload: Transfers the JAR file to the deployment server
- Service Configuration: Creates a systemd service with appropriate Java options
- Health Check: Validates the application is running correctly
While this pipeline is optimized for Java applications, it can be adapted for other application types by modifying the build, artifact, and service configuration. See the GENERIC_ADAPTATION.md file for guidance on adapting this pipeline for other application types.
The pipeline supports deploying to multiple servers within a single environment:
- Configuration: Set
MULTI_SERVER_DEPLOYMENT: "true"
and define servers in theDEPLOY_HOSTS
array - Sequential Deployment: Servers are deployed to one after another
- Validation: Each server must pass health checks before proceeding to the next
- Fallback: If multi-server mode is disabled, falls back to single server (
DEPLOY_HOST
) - Error Handling: If deployment fails on any server, the entire job fails
- Testing: Comprehensive testing validates multi-server deployment and rollback
variables:
MULTI_SERVER_DEPLOYMENT: "true"
DEPLOY_HOSTS: '["server1.example.com", "server2.example.com"]'
DEPLOY_HOST: "server1.example.com" # Fallback for single-server mode
To ensure controlled deployments and comply with the May 2025 GitLab protected branch updates:
- Manual Approval: All environment promotions require manual approval
- Configuration: Set
AUTO_PROMOTION: "false"
in environment variables - Branch Protection: Works with GitLab's protected branch rules
- Safety Check: Pipeline validates that deployments are manually triggered
- Compliance: Helps maintain compliance with TADS CO-014-04 standards
variables:
AUTO_PROMOTION: "false" # Prevents automatic promotion between environments
Our comprehensive testing framework ensures that the pipeline works correctly in all scenarios:
The diagram above illustrates the comprehensive testing of deployment and rollback functionality. It shows how the testing framework validates all aspects of the pipeline.
Our testing philosophy is focused on consistency and reliability. We use the exact same CI files in both production and testing environments, ensuring complete consistency between test and production deployments.
To run all tests in sequence:
./tests/run_all_tests.sh
This will execute all tests from basic to comprehensive, providing clear feedback on each test's success or failure.
Test Type | Command | Purpose |
---|---|---|
Basic Structure | ./tests/test_pipeline.sh |
Validates pipeline structure and job dependencies |
GitLab CI Local | ./tests/gitlab_ci_local_comprehensive.sh |
Tests all pipeline actions using gitlab-ci-local |
Systemd Service | ./tests/test_systemd_rollback.sh |
Tests systemd service handling and rollback |
Comprehensive | ./tests/comprehensive_pipeline_test.sh |
Full validation including edge cases and multi-server deployments |
Our comprehensive test suite validates:
-
Build and Deployment
- Artifact creation and packaging
- Deployment to target environments
- Directory structure creation
- Symlink management
-
Systemd Service Handling
- Service file creation
- Service lifecycle (enable, start, stop)
- Service status verification
-
Rollback Functionality
- Manual rollback capability
- Automatic rollback on failure
- Rollback to specific versions
-
Multi-Server Deployments
- Sequential deployment to multiple servers
- Health check validation on each server
- Rollback across multiple servers
-
Edge Cases
- Handling of missing directories
- Recovery from failed deployments
- Behavior with invalid configurations
- Validate branch permissions and auto-promotion settings
- Build the Java application using Maven/Gradle
- Create deployment directories
- Backup current deployment
- Upload Java JAR artifact
- Configure Java-specific systemd service
- Update symlinks for atomic deployment
- Start the Java service
- Perform health checks
- Clean up old deployments
- Send notifications
The pipeline includes a comprehensive testing framework to ensure it works correctly in all environments:
- Principle: Ensuring consistency between test and production environments
- Scripts: Multiple test scripts with increasing levels of coverage (see
/tests
directory) - Components:
- Basic pipeline structure validation using gitlab-ci-local
- systemd service testing with rollback functionality
- Multi-server deployment simulation
- Edge case handling for service failures
- Containers: Tests run in Podman containers with systemd support to simulate real environments
- Comprehensive Coverage: All aspects of the pipeline are tested, including deployment, rollback, and service management
See the tests/README.md for detailed information about the testing approach.
- Production:
main
,master
,production
- GitLab CI/CD runner with shell executor
- SSH access to deployment servers
- Java 17 installed on deployment servers
- Maven for building the application
- Update server hostnames in
ci/variables.yml
- Configure environment-specific variables as needed
- Adjust branch rules in the deployment jobs if necessary
- Set up notification preferences
- Modular configuration using
include
- Template inheritance with
extends
- Environment-specific configurations
- Reusable shell functions
- Atomic deployments
- Health validation
- Automated rollbacks
- Retention policies
- Protected variables for sensitive information
- User-specific deployments
- Systemd service isolation
- Branch protection rules
Problem | Symptoms | Solution |
---|---|---|
YAML Parsing Errors | Pipeline fails with found character that cannot start any token or similar |
• Check indentation in YAML files, especially in functions.yml • Ensure shell functions are properly indented within YAML structure • Validate YAML with gitlab-ci-lint |
Job Dependency Errors | Pipeline fails with job not found or needs not defined |
• Verify job names match between dependencies • Check that all referenced jobs exist in the pipeline • Ensure job names are consistent across all files |
Systemd Container Issues | Service fails to start in container with Failed to connect to bus |
• Ensure container has privileged access • Mount /sys/fs/cgroup correctly• Run container with /sbin/init as PID 1 |
Java Application Errors | Application fails to start with OutOfMemoryError |
• Check JVM memory settings in RUNTIME_OPTS • Verify Java version compatibility • Ensure proper Spring profiles are activated |
SSH Authentication Failures | Deployment fails with Permission denied (publickey) |
• Verify SSH keys are correctly configured • Check APP_USER has proper permissions• Ensure known_hosts is properly configured |
Health Check Failures | Deployment completes but health check fails | • Verify application is actually running • Check HEALTH_CHECK_URL is correct• Increase HEALTH_CHECK_RETRIES and HEALTH_CHECK_DELAY |
Problem: Build job succeeds but artifacts aren't collected
Solution:
# Check your artifact path configuration
variables:
ARTIFACT_PATH: "target/*.jar" # Make sure this matches your build output
Problem: Service starts but immediately fails with class not found
Solution:
- Check your
START_COMMAND
includes the correct classpath - Verify all dependencies are included in your JAR (use
jar -tf your-app.jar
) - Update your service file to include any external dependencies:
function create_systemd_service() {
# Add classpath to the service file
echo -e "[Unit]\nDescription=${APP_NAME} Application\nAfter=network.target\n\n[Service]\nType=simple\nUser=${APP_USER}\nWorkingDirectory=${WORKING_DIRECTORY}\nExecStart=${JAVA_HOME}/bin/java -cp ${WORKING_DIRECTORY}/*:${WORKING_DIRECTORY}/lib/* ${MAIN_CLASS}\n${env_vars}SuccessExitStatus=143\nTimeoutStopSec=10\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target" > ${APP_NAME}.service
}
Add to your ci/variables.yml
:
variables:
DEBUG: "true"
Add to your ci/variables.yml
:
variables:
CI_TEST_MODE: "true"
# View service logs
journalctl --user-unit=your-app-name
# Follow logs in real-time
journalctl --user-unit=your-app-name -f
# View only errors
journalctl --user-unit=your-app-name -p err
# Check symlinks
ls -la /opt/app/your-app/current
# Verify service file
cat ~/.config/systemd/user/your-app.service
# Check deployment history
ls -la /opt/app/your-app/deployments/
Create a diagnostic script in your repository at scripts/diagnose.sh
:
#!/bin/bash
APP_NAME="your-app-name"
DEPLOY_DIR="/opt/app/${APP_NAME}"
echo "=== Deployment Diagnostics ==="
echo "Checking service status..."
systemctl --user status ${APP_NAME}.service
echo "\nChecking deployment structure..."
ls -la ${DEPLOY_DIR}/current
echo "\nChecking recent logs..."
journalctl --user-unit=${APP_NAME} -n 20
echo "\nChecking disk space..."
df -h ${DEPLOY_DIR}
echo "\nChecking Java version..."
java -version
echo "\nDiagnostics complete."