CI/CD for Spring Boot with GitHub Actions

Khimananda Oli 7 min read Programming and Languages
CI/CD for Spring Boot with GitHub Actions

By Khimananda Oli | Last reviewed: August 2026

Shipping Java microservices reliably requires more than just running mvn package; it demands a reproducible, secure automation layer that catches regressions before they reach production. Implementing CI/CD for Spring Boot with GitHub Actions gives you tight integration with your repository, native Maven/Gradle caching, and OIDC-based cloud authentication without managing Jenkins servers. This guide walks through building a hardened, audit-ready pipeline that handles everything from unit tests to containerized deployments.

Git Push / PRmain / feature/*Build & TestJDK 21 + MavenDocker BuildMulti-stage ImageDeployOIDC / K8s / VMArtifact Registry (GHCR / ECR) + SBOM + Security Scan
End-to-end CI/CD for Spring Boot with GitHub Actions: source trigger through tested artifact to deployment target

How do you structure a CI/CD for Spring Boot with GitHub Actions workflow?

A common mistake is cramming every step into a single job, which prevents parallelism and makes debugging painful. Split your pipeline into logical jobs: test, build-image, and deploy. Use explicit needs dependencies so the image only builds after tests pass, and deployment only occurs after a successful image push. Pin all third-party actions to full commit SHAs rather than mutable tags like v4 to prevent supply-chain attacks — a critical practice for any team pursuing SOC 2 or ISO 27001 compliance.

Core workflow skeleton

name: Spring Boot CI/CD
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

permissions:
  contents: read
  packages: write
  id-token: write   # Required for OIDC

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'
      - name: Run tests
        run: ./mvnw -B verify --file pom.xml

  build-and-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
      - name: Push to GHCR
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

This structure ensures that broken code never produces a deployable artifact. The if guard on the build job prevents unnecessary image creation on pull requests, saving both time and registry storage costs.

How do you optimize Maven builds and dependency caching in GitHub Actions?

Spring Boot projects pull hundreds of transitive dependencies. Without caching, each workflow run downloads 200–500 MB of JARs, adding 2–4 minutes of pure network latency. The actions/setup-java action includes built-in Maven caching that hashes your pom.xml files automatically. However, many teams miss that the cache key must account for multi-module projects correctly.

  • Use the wrapper: Always invoke ./mvnw instead of system mvn to guarantee version consistency between local dev and CI.
  • Enable batch mode: Pass -B to suppress interactive prompts and reduce log noise.
  • Parallelize modules: Add -T 1C to use one thread per CPU core during compilation and testing.
  • Skip redundant plugins: In CI, disable reporting plugins not needed for verification: -DskipITs=false -Denforcer.skip=true.

If your project uses Gradle instead of Maven, swap cache: 'maven' for cache: 'gradle' in the setup-java step. The underlying mechanism is identical, but the cache keys differ. For monorepos with multiple Spring Boot services, consider using monorepo strategies with path filters to avoid rebuilding unchanged services.

What is the best Dockerfile strategy for Spring Boot in CI?

Never copy your entire source tree into a Docker image and run mvn package inside the container. This bloats layers, slows builds, and leaks build tools into production images. Use a multi-stage build where compilation happens in an ephemeral stage and only the runtime JRE plus the final JAR survive to the output image.

# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY .mvn .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -B
COPY src src
RUN ./mvnw package -DskipTests -B

# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-jar", "app.jar"]

The dependency:go-offline step creates a dedicated layer for dependencies. Since application code changes far more frequently than dependencies, this layer stays cached across commits. The Alpine JRE base reduces the final image to ~90 MB versus ~300 MB for full JDK images. Always run as a non-root user (appuser) — this is a non-negotiable security baseline for containerized workloads.

Builder Stage (JDK 21)COPY pom.xml + .mvndependency:go-offline (cached layer)COPY src/mvn package → app.jarCOPY --from=builderRuntime Stage (JRE Alpine)adduser appuser (non-root)app.jar (~90 MB total)USER appuser + ENTRYPOINTNo JDK, No Maven, No Source
Multi-stage Docker build isolates build dependencies from the minimal runtime image for Spring Boot containers

How do you manage secrets and credentials securely in Spring Boot pipelines?

Hardcoding database passwords or API keys in workflow files is the fastest way to fail a security audit. GitHub Actions provides encrypted secrets at the repository and organization level, but how you inject them matters. Never echo secrets to logs. Use environment variables scoped to specific steps, and prefer OIDC over static access keys whenever possible.

MethodSecurity LevelBest ForRisk If Compromised
Repository SecretsMediumSingle-service DB passwords, API tokensLimited to one repo
Organization SecretsHighShared registry creds, cloud provider rolesAll repos with access
OIDC (id-token)HighestAWS/Azure/GCP deploymentsNone (short-lived, no stored keys)
Vault / External SMHighestDynamic secrets, rotation policiesRequires Vault auth config

For AWS deployments, configure an IAM role trust policy that accepts GitHub’s OIDC provider. Then use aws-actions/configure-aws-credentials@v4 with role-to-assume instead of aws-access-key-id. This eliminates long-lived credentials entirely. For database migrations in CI, create a dedicated read-write user scoped to the test schema — never reuse production credentials. Teams handling sensitive data should review secrets management best practices before going live.

How do you deploy Spring Boot applications safely from GitHub Actions?

Deployment is where most pipelines break under real-world pressure. A naive docker run or kubectl apply causes downtime and offers no rollback path. Structure deployments as discrete jobs with health checks, and always tag images with immutable identifiers (commit SHA), not mutable tags like latest.

  1. Tag immutably: Use ${{ github.sha }} as the primary tag. Optionally add main-${{ github.run_number }} for human readability.
  2. Separate deploy job: Keep deployment logic out of the build job. This allows re-running a failed deploy without rebuilding.
  3. Add health verification: After deploying, curl the /actuator/health endpoint. Fail the job if it doesn’t return 200 within 60 seconds.
  4. Implement rollback: Store the previous working SHA. On health check failure, redeploy that SHA automatically.
  5. Gate production: Use GitHub Environments with required reviewers for production deploys. Staging can auto-deploy on merge to main.

For Kubernetes targets, integrate with GitOps tools like ArgoCD rather than applying manifests directly from Actions. This keeps the cluster state declarative and auditable. See setting up GitOps with ArgoCD for a pattern that pairs well with GitHub Actions as the CI provider. For traditional VPS deployments, use SSH with key-based auth stored in secrets, and always restart via systemd with --wait to confirm service activation.

Deploy New SHAImmutable TagHealth Check/actuator/healthPASSProd ApprovalEnvironment GateLive ✓FAILAuto RollbackPrevious Known-Good SHA
Deployment safety gates in CI/CD for Spring Boot: health verification, approval gates, and automatic rollback protect production

Making Your CI/CD for Spring Boot with GitHub Actions Production-Ready

A working pipeline is just the starting point. To make your CI/CD for Spring Boot with GitHub Actions truly production-grade, add observability from day one: emit structured logs during builds, track pipeline duration metrics, and alert on repeated failures. Integrate static analysis with SonarQube as a quality gate before merging. Sign your container images with Sigstore cosign to establish supply chain provenance. Review your workflow permissions quarterly — remove packages: write from PR-triggered runs and restrict environment access to protected branches only. If you need help hardening your Java delivery pipeline or preparing for a compliance audit, reach out to discuss your infrastructure.

Frequently Asked Questions

Create a workflow YAML file in .github/workflows defining jobs for building, testing, and deploying. Use actions/setup-java with Eclipse Temurin 21 and gradle/actions or maven-actions to cache dependencies and execute build tasks efficiently within your Spring Boot pipeline.

Use Eclipse Temurin JDK 21 as it is the current LTS release supported by Spring Boot 3.x. Configure actions/setup-java with distribution temurin and java-version 21 to ensure compatibility and access to modern virtual thread features in your CI environment.

Yes, enable caching via actions/setup-java.

Use testcontainers with Docker support enabled in your workflow. Add docker/setup-docker-action before running ./mvnw verify to provide a container runtime. This allows database and message queue integration tests to execute reliably without external service dependencies during CI runs.

Build a Docker image using docker/build-push-action, push to ECR, then update the ECS task definition. Use aws-actions/configure-aws-credentials with OIDC for secure authentication. Finally, deploy via aws-actions/amazon-ecs-deploy-task-definition to roll out the new Spring Boot container safely.

Yes, public repositories get unlimited minutes. Private repos receive 2000 monthly minutes on standard runners, which typically suffices for small-to-medium Spring Boot projects. Monitor usage in billing settings and consider self-hosted runners if builds exceed limits frequently.

Store credentials as encrypted repository or environment secrets. Never hardcode values in workflows. Use OpenID Connect for cloud provider authentication instead of long-lived access keys. Rotate secrets regularly and restrict secret access to specific deployment branches or environments only.

Increase heap size by setting JAVA_OPTS=-Xmx4g -XX:MaxMetaspaceSize=512m in your workflow env block. Standard runners have 7GB RAM; large Spring Boot builds with many modules often exceed default JVM limits during compilation or test execution phases.

Use matrix strategy to split modules across multiple jobs. Define module names in the matrix array and run ./mvnw -pl ${{ matrix.module }} -am verify concurrently. This reduces total pipeline time significantly for multi-module Spring Boot monorepos with independent components.

Standard ubuntu-latest runners suffice for most projects. Use larger runners like ubuntu-latest-8-cores only if builds consistently exceed 15 minutes or require extensive memory for integration tests. Self-hosted runners offer better performance-to-cost ratios for high-frequency Spring Boot CI workloads.

Specify branches under the push trigger.

Authenticate to your registry using docker/login-action with GITHUB_TOKEN or personal access tokens. Build with docker/build-push-action specifying tags and labels. Enable layer caching via type=gha to speed up subsequent builds. Push only after successful test completion to avoid publishing broken images.

Yes, for most teams. GitHub Actions offers native repository integration, simpler YAML configuration, and managed infrastructure without server maintenance. Jenkins remains preferable for complex legacy pipelines, air-gapped environments, or organizations requiring extensive plugin ecosystems not available in the GitHub Actions marketplace.

Enable step debugging by re-running with debug logging enabled. Download artifacts containing test reports and logs. Use tmate/action-tmate for interactive SSH sessions into the runner to inspect state manually. Check job summaries for specific error messages and stack traces from failed Maven or Gradle tasks.

Avoid running full builds on every pull request without path filtering. Do not skip tests to speed up pipelines. Never store secrets in workflow files or logs. Always pin action versions to SHA hashes instead of mutable tags to prevent supply chain attacks and ensure reproducible builds over time.