Mid 9 min · May 23, 2026

CI/CD for Spring Boot with GitHub Actions

Master CI/CD pipelines for Spring Boot with GitHub Actions: multi-stage builds, Docker, AWS ECS/Kubernetes deploy, SonarQube, secrets management.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Define a multi-stage workflow: build → test → docker build → push → deploy
  • Cache Maven/Gradle dependencies with actions/cache keyed on lockfile hash
  • Store secrets in GitHub Secrets and inject via env: in workflow steps
  • Use matrix builds to test against multiple JDK versions simultaneously
  • Integrate SonarQube with sonar-maven-plugin and SONAR_TOKEN secret
✦ Definition~90s read
What is CI/CD for Spring Boot with GitHub Actions?

GitHub Actions is GitHub's built-in CI/CD platform that runs workflows defined as YAML files in the .github/workflows/ directory of your repository. Workflows are triggered by events (push, pull_request, schedule, workflow_dispatch) and consist of jobs that run on hosted or self-hosted runners.

Think of GitHub Actions as a robotic assembly line in a factory.

Each job contains steps that execute shell commands or call reusable actions from the GitHub Marketplace.

For Spring Boot applications, a CI/CD pipeline typically spans four concerns: (1) build and test — compiling source, running unit/integration tests, generating code coverage reports; (2) static analysis — SonarQube or Checkstyle to enforce code quality gates; (3) artifact packaging — building a Docker image, tagging it with the Git SHA, and pushing to a container registry (ECR, GHCR, Docker Hub); (4) deployment — updating a Kubernetes Deployment or ECS service to pull the new image. Each stage is a dependency-ordered job in the workflow file, ensuring failures halt the pipeline before bad artifacts reach production.

Plain-English First

Think of GitHub Actions as a robotic assembly line in a factory. Every time a developer pushes code, the robots automatically compile the product, run quality checks, package it into a shipping container (Docker image), and deliver it to the warehouse (production servers) — all without human intervention. If any station fails, the line stops and alerts the team before a defective product ships.

In 2022, a major fintech team I consulted for was deploying Spring Boot services manually via SSH. A developer fat-fingered a JAR filename at 11 PM on a Friday and took down payments processing for 40 minutes. The incident cost $200K in chargebacks and led to a three-week post-mortem. The fix was a proper CI/CD pipeline — something that should have existed from day one.

GitHub Actions has become the default CI/CD platform for Spring Boot projects because it lives where your code lives, requires zero infrastructure to bootstrap, and has a rich marketplace of pre-built actions. But most tutorials show only the happy path: compile, test, done. Production pipelines are far more nuanced.

A production-grade GitHub Actions pipeline for Spring Boot needs to handle dependency caching aggressively — cold Maven builds pull 500MB+ of artifacts. It needs matrix builds to catch JDK version drift. It needs Docker layer caching so a 3-minute image build doesn't become your pipeline bottleneck. It needs gated deployments so that staging gets every commit but production requires a manual approval.

SonarQube integration is non-negotiable for enterprise teams. Static analysis catches security vulnerabilities (SQL injection, XXE, SSRF) that unit tests will never find. Wiring sonar:analyze into your pipeline with quality gates that break the build on new critical findings is the difference between a security-conscious team and a breach waiting to happen.

This guide walks through a battle-tested GitHub Actions workflow for Spring Boot: from the first push that compiles your code all the way to a zero-downtime rolling deployment on AWS ECS or Kubernetes, with every production gotcha documented.

Multi-Stage Workflow Architecture

A production GitHub Actions workflow for Spring Boot should be structured as a directed acyclic graph of jobs, not a single monolithic job. Each job runs on its own fresh runner, which means you need to explicitly pass artifacts between jobs using actions/upload-artifact and actions/download-artifact. This isolation is a feature, not a bug — it ensures your test environment doesn't leak state into your build environment.

The canonical job order is: build-and-testsonarqube (runs in parallel with test if you have a separate test report upload) → docker-build-pushdeploy-stagingintegration-test-stagingdeploy-production. The deploy-production job should require a manual approval using GitHub's environment protection rules with required reviewers.

One critical mistake teams make is running all steps in a single job for simplicity. This means a Docker build failure wastes 5 minutes of test time on a re-run. Split jobs properly and use needs: to express dependencies. Use if: github.ref == 'refs/heads/main' to restrict deployment jobs to the main branch, preventing feature branch pushes from triggering deploys.

For monorepos containing multiple Spring Boot services, use path filters with dorny/paths-filter to only build and deploy services that have changed. Running a full pipeline for every service on every commit is a waste of runner minutes and slows down developer feedback loops significantly.

Never Put Secrets in Environment Variables at the Workflow Level
Defining secrets at the top-level env: block makes them available to all jobs including third-party actions. Always scope secrets to the specific step that needs them using the step-level env: block. This limits blast radius if a malicious action exfiltrates environment variables.
Production Insight
At a fintech client, splitting the single 12-minute job into 4 parallel jobs reduced P50 feedback time from 12 minutes to 4 minutes — developers stopped context-switching away while waiting.
Key Takeaway
Structure your pipeline as a job DAG with artifact passing between stages, not a single monolithic job with all steps sequential.

Maven and Gradle Dependency Caching

Dependency caching is the single highest-ROI optimization in Spring Boot pipelines. A cold Maven build for a medium Spring Boot application (50+ dependencies) downloads 300-600MB of artifacts. On a GitHub-hosted runner with ~100 Mbps bandwidth, that's 30-60 seconds of pure network I/O per run. Multiply that by 50 builds per day across a team and you're burning 25-50 minutes of developer wait time daily on artifact downloads alone.

The correct cache key strategy is a two-level key: a primary key that is an exact hash of all POM files, and a restore-keys fallback that matches any cache from the same OS. When dependencies don't change (most commits), you get 100% cache hits and spend ~2 seconds on cache restore instead of 60 seconds downloading. When you add a dependency, the primary key misses, the fallback key retrieves the old cache, Maven downloads only the new artifacts, and the cache saves the updated state for future runs.

For Gradle, use actions/setup-java with cache: gradle which handles the Gradle wrapper cache, build cache, and dependency cache automatically. For Maven, use cache: maven in setup-java or manage it manually with actions/cache if you need fine-grained control.

Docker layer caching is equally important. Use GitHub Actions Cache backend (cache-from: type=gha, cache-to: type=gha,mode=max) with docker/build-push-action. Combine this with a properly layered Dockerfile (dependencies layer first, application layer last) to achieve near-instant Docker builds when only application code changes.

Use Spring Boot Layered JARs for Maximum Docker Cache Efficiency
Spring Boot's layered JAR feature splits the artifact into 4 layers: dependencies (changes rarely), spring-boot-loader (changes with Boot version), snapshot-dependencies (changes on SNAPSHOT updates), and application (changes every build). This means typical builds only invalidate the 2-10MB application layer, not the full 80MB+ JAR.
Production Insight
Implementing layered JARs reduced Docker build time from 3m 20s to 18s for a team with 100+ daily commits — the dependencies layer stayed cached for days at a time.
Key Takeaway
Cache at two levels: Maven/Gradle dependencies keyed on lockfile hash, and Docker layers using Spring Boot's layered JAR extraction.

Matrix Builds and SonarQube Integration

Matrix builds allow you to test your Spring Boot application against multiple JDK versions, operating systems, or database backends in parallel. This is essential for library authors and teams that need to support multiple JDK LTS versions (17, 21) or validate that their service works with both PostgreSQL 14 and 15.

The matrix strategy generates a Cartesian product of all specified dimensions. A matrix of jdk: [17, 21] and database: [postgres:14, postgres:15] produces 4 parallel jobs. Each job gets the matrix variables via ${{ matrix.jdk }} and ${{ matrix.database }}. Use fail-fast: false in production matrix configs so a failure in one combination doesn't cancel all other combinations — you want to see the full failure surface.

SonarQube integration requires careful setup. The most common mistake is not passing fetch-depth: 0 to actions/checkout, which truncates Git history and breaks SonarQube's blame data, leading to incorrect 'new code' calculations. SonarQube uses Git blame to determine which code is 'new' since the last analysis and applies different quality gate thresholds to new vs existing code.

For pull request analysis, SonarQube needs to know the base branch and PR number to decorate the PR with inline comments. Pass sonar.pullrequest.key, sonar.pullrequest.branch, and sonar.pullrequest.base from the GitHub Actions context. The GITHUB_TOKEN secret (automatically provided) needs to be passed as sonar.pullrequest.github.token for PR decoration to work.

fetch-depth: 0 Is Not Optional for SonarQube
The default actions/checkout does a shallow clone with only the latest commit. SonarQube requires full Git history to calculate blame information for the 'new code' period. Without fetch-depth: 0, SonarQube treats all code as new and your quality gate thresholds won't work correctly.
Production Insight
A team discovered their JDK 17 → 21 migration broke 3 tests due to changed reflective access behavior — matrix builds caught it in CI before it hit the main branch.
Key Takeaway
Use fail-fast: false in matrix builds to see the complete failure surface, and always pass fetch-depth: 0 for SonarQube.

Secrets Management and Security Hardening

GitHub Secrets is sufficient for most teams, but it has important limitations: secrets are flat key-value pairs with no hierarchy, no versioning, no rotation automation, and no audit log of which workflow used which secret. For regulated industries (finance, healthcare), you need a proper secrets management solution: AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault.

The OIDC (OpenID Connect) approach for AWS authentication eliminates the need to store long-lived AWS credentials as GitHub Secrets entirely. Instead, GitHub Actions requests a short-lived OIDC token, exchanges it for AWS credentials via STS AssumeRoleWithWebIdentity, and the credentials expire after the job completes. This is the modern best practice — there are no static credentials to rotate, no risk of secret sprawl, and IAM policies can restrict which repos and branches can assume which roles.

For environment-specific secrets (dev/staging/prod), use GitHub Environments with environment-scoped secrets. The production environment should require manual approval and have protection rules preventing deployment from non-main branches. This means even if a developer pushes directly to main, they cannot bypass the approval gate for production deployment.

Security hardening for GitHub Actions workflows themselves: pin all third-party actions to SHA hashes, not tags. A malicious actor can push a new tag to a public action repo and inject code into your pipeline. uses: actions/checkout@v4 is vulnerable to a tag overwrite. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 is immutable. Use step-security/harden-runner to restrict network egress from runner steps.

Use OIDC Instead of Long-Lived AWS Credentials
AWS OIDC federation with GitHub Actions is a 30-minute one-time setup that eliminates the need to ever rotate AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in GitHub Secrets. The credentials are ephemeral (15-60 minute TTL), can be scoped to specific repos and branches in the IAM trust policy, and leave a clean CloudTrail audit trail.
Production Insight
A startup had their AWS keys exfiltrated via a malicious transitive dependency in a GitHub Action. Switching to OIDC the next day closed that entire attack vector permanently.
Key Takeaway
Use AWS OIDC federation to eliminate stored credentials, and pin all third-party actions to immutable SHA hashes.

Deploy to AWS ECS and Kubernetes

Deploying to AWS ECS from GitHub Actions requires three steps: update the task definition JSON with the new image URI, register the new task definition revision, and update the ECS service to use it. AWS provides official actions for each step. The aws-actions/amazon-ecs-render-task-definition action substitutes the image URI into a task definition template stored in your repository, and aws-actions/amazon-ecs-deploy-task-definition registers and deploys it.

For Kubernetes deployments, the approach depends on your cluster access model. Direct kubectl access (using a kubeconfig stored as a GitHub Secret) is simple but grants broad cluster access to the pipeline. The better pattern is to use GitOps: the pipeline pushes a new image tag to a Helm values file or Kustomize overlay in a separate GitOps repository, and ArgoCD or Flux detects the change and deploys it. This separates CI (GitHub Actions) from CD (ArgoCD), gives you deployment history in Git, and allows rollback by reverting a commit.

For both ECS and Kubernetes, wait for the deployment to stabilize before marking the pipeline as successful. For ECS, use aws ecs wait services-stable. For Kubernetes, use kubectl rollout status deployment/my-app --timeout=5m. Failing to wait means your pipeline shows green while your application is still rolling out — or worse, while it's stuck in a crash loop.

GitOps Gives You Free Deployment Rollback
When using ArgoCD or Flux with a GitOps repository, rolling back a bad deployment is a git revert and push — the CD system detects the change and reverts the cluster state automatically. Compare this to ECS where you need to re-run the pipeline with an older image tag or manually update the task definition.
Production Insight
A team using direct kubectl in CI couldn't roll back quickly because the pipeline had to re-run from scratch. After switching to GitOps, rollbacks took 90 seconds instead of 8 minutes.
Key Takeaway
Always wait for deployment stability in CI (wait-for-service-stability: true or kubectl rollout status) before marking the job successful.

Notifications, Observability, and Pipeline Hygiene

A CI/CD pipeline that silently fails is worse than no pipeline. Production teams need immediate, actionable notification when deployments fail. GitHub Actions natively supports Slack, PagerDuty, and email notifications via marketplace actions or simple webhook calls. The notification should include: which branch/PR caused the failure, which job failed, a link to the failed job logs, and the Git SHA for context.

Pipeline metrics matter as much as application metrics. Track pipeline duration trends over time — a build that was 3 minutes and is now 8 minutes signals accumulated technical debt (test suite growth, unoptimized Docker builds, cache misses). Use GitHub's built-in Actions usage reports or export metrics to Datadog/Grafana via the GitHub API.

Conditional notifications prevent alert fatigue. Only notify on failure (not success), and deduplicate — if three commits fail in quick succession on the same branch, send one notification, not three. For production deployments specifically, send a success notification to a deployment log channel so the team has a clear audit trail of what deployed when.

Pipeline hygiene: delete old workflow runs to keep the Actions tab navigable. Use concurrency: groups to cancel in-progress runs when a new commit is pushed to the same branch — there's no point finishing a build for a commit that's already been superseded. Set cancel-in-progress: true for feature branches but not for main, where you want every deploy to complete.

Use Concurrency Groups to Avoid Wasted Runner Minutes
Without concurrency groups, pushing 5 commits in quick succession launches 5 parallel pipeline runs. For feature branches, only the latest run matters. Setting cancel-in-progress: true in a concurrency group keyed on the branch name ensures only one pipeline runs at a time per branch, saving 80% of runner minutes in active development sessions.
Production Insight
A team burning $800/month on GitHub Actions reduced their bill to $180/month solely by adding concurrency groups — developers were pushing work-in-progress commits constantly.
Key Takeaway
Use concurrency groups with cancel-in-progress for feature branches, and always add failure notifications scoped to the specific jobs that matter most.

Containerize Your Spring Boot App Like a Pro (Buildpacks vs. Dockerfile)

You have two paths to package your Spring Boot 3.x app for CI/CD: a handwritten Dockerfile or Spring Boot's native Buildpacks support. Buildpacks win for most teams because they eliminate Dockerfile drift and security debt. They auto-detect your JDK version, layer your dependencies correctly, and produce OCI-compliant images without you touching a single FROM instruction. Your GitHub Action just needs the Paketo builder. The Buildpacks output is a lean image with optimized layer caching — your app code changes only invalidate the application layer, not the whole image. That shaves minutes off your deploy pipeline. Only drop to a custom Dockerfile when you need distroless images or exotic base layers like alpine-glibc. Even then, use multi-stage builds. Your production pipeline should never run 'docker build' with a single-stage file that copies your fat jar into a JDK image — that's a 400MB image for a 20MB app. That's amateur hour.

.github/workflows/buildpacks.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# io.thecodeforge — spring boot 3.x buildpacks action
name: Buildpacks CI
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Build image with Paketo Buildpacks
        run: |
          ./mvnw spring-boot:build-image \
            -Dspring-boot.build-image.imageName=ghcr.io/${{ github.repository }}:latest
      - name: Push to registry
        run: |
          echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/${{ github.repository }}:latest
Output
Successfully built image ghcr.io/myorg/payment-service:latest
Pushed to GitHub Container Registry in 47s
Production Trap:
Buildpacks images default to the 'paketobuildpacks/builder:base' builder which includes a shell. For hardened production, switch to 'paketobuildpacks/builder:tiny' — it drops the shell and reduces attack surface. Add '-Dspring-boot.build-image.builder=paketobuildpacks/builder:tiny' to your Maven command.
Key Takeaway
Always prefer Buildpacks over handwritten Dockerfiles unless you need a distroless base image. Buildpacks auto-patch CVEs in base layers. Your Dockerfile won't.

Docker Compose in CI? Nope. Use Testcontainers for Integration Tests

Don't run docker-compose up inside your GitHub Action. That's a recipe for flaky builds, port conflicts, and 3-minute container spin-up times. Spring Boot 3.x has Testcontainers integration built-in. Your CI pipeline should fire up a PostgreSQL container, your Redis cache, and your Kafka broker through @ServiceConnection annotations — directly in your test code. GitHub Actions runners have Docker sockets. Testcontainers uses them to create containers on-demand. Your pipeline stays clean: no docker-compose.yml in your repo, no environment-specific compose overrides, no 'docker-compose down' failure messing up your pipeline. Each test class gets its own container lifecycle. If a container crashes, the test fails fast with a clear error. The competitor pages show Docker Compose for local dev — fine. But for CI, Testcontainers is the only production-grade answer. Your pipeline should not care about port mappings or network names.

PaymentServiceIntegrationTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — spring boot 3.x testcontainers
@SpringBootTest
@Testcontainers
class PaymentServiceIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Container
    @ServiceConnection
    static GenericContainer<?> redis = new GenericContainer<>(
            DockerImageName.parse("redis:7-alpine"))
            .withExposedPorts(6379);

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldProcessPaymentWithRealDatabaseAndCache() {
        Payment result = paymentService.process(new PaymentRequest(100.00, "USD"));
        assertThat(result.id()).isNotNull();
        // Container spins up in 4s, test runs, container dies.
    }
}
Output
Tests run: 12, Failures: 0, Errors: 0, Time elapsed: 8.432 sec
Hot Tip:
Add 'ryuk.container.timeout=60s' to your test properties. Ryuk is Testcontainers' cleanup daemon. Without it, orphaned containers accumulate on your GitHub runner and eat disk space. Your pipeline will mysteriously fail after 10 runs.
Key Takeaway
Testcontainers + @ServiceConnection is the only sane way to integration test in CI. No docker-compose. No env-specific config. Just containers that die when your tests pass.
● Production incidentPOST-MORTEMseverity: high

The $47K Broken Pipeline Nobody Noticed

Symptom
Security audit found a SQL injection vulnerability in production that had been present for 6 weeks. SonarQube had flagged it as a Critical issue on day one.
Assumption
The team assumed the SonarQube step in CI was blocking the pipeline on quality gate failures.
Root cause
The SonarQube analysis step used mvn sonar:analyze but lacked the sonar.qualitygate.wait=true property. Analysis uploaded results asynchronously, the step returned exit 0 before the quality gate computed, and the pipeline continued regardless of findings.
Fix
Add -Dsonar.qualitygate.wait=true to the Maven command. This makes the plugin poll SonarQube until the quality gate result is available and exits non-zero on failure. Also add a dedicated check-quality-gate step using the sonarsource/sonarqube-quality-gate-action.
Key lesson
  • Never assume a CI step is blocking unless you've verified the exit code behavior.
  • Test your pipeline's failure path explicitly by temporarily introducing a known vulnerability and confirming the build breaks.
Production debug guideSymptom → root cause → fix5 entries
Symptom · 01
Pipeline takes 8+ minutes just on Maven dependency download
Fix
The actions/cache step is either misconfigured or the cache key changes every run. Ensure you key on ${{ hashFiles('**/pom.xml') }} not on the branch or SHA. Verify the cache hit rate in the Actions UI under the cache step's output. If restoreKeys are missing, add a fallback key without the hash so partial cache hits work.
Symptom · 02
Docker build pushes successfully but ECS task fails to start with 'image not found'
Fix
The ECR repository region in the image URI doesn't match the region where the ECS service is running. Check the pushed image URI in the pipeline logs vs the task definition ARN region. Also verify the task execution role has ecr:GetAuthorizationToken and ecr:BatchGetImage permissions — missing IAM permissions produce the same 'not found' error from the agent's perspective.
Symptom · 03
SonarQube analysis fails with 'Could not find a default branch' on PRs
Fix
SonarQube requires the main branch to exist in its project before PR decoration works. Run the analysis on the main branch first (usually via a push trigger to main) to establish the baseline. Then PR analyses compare against that baseline. Also ensure sonar.pullrequest.base and sonar.pullrequest.branch are passed correctly from the GitHub context variables.
Symptom · 04
Matrix build job for JDK 21 fails but JDK 17 passes — same test class
Fix
Likely a reflection or sealed class API that changed between LTS versions, or a test that relies on HashMap ordering which changed. Run the failing test locally with JDK 21 to get the full stack trace. Common culprits: sun.misc.Unsafe usage in Mockito, Jackson serialization of records, or Spring Security's default SecurityFilterChain API changes between Boot 3.x minor versions.
Symptom · 05
Deployment job hangs indefinitely after updating ECS service
Fix
The aws ecs wait services-stable command waits up to 10 minutes by default with no output. The ECS service may be in a deployment loop if the new task fails health checks. Add explicit timeout to the wait command (--max-attempts 40), and in parallel check ECS service events in the AWS console. Most common causes: the new image fails the Spring Boot health check at /actuator/health, or the task has insufficient memory and is OOM-killed before Boot finishes starting.
★ GitHub Actions Debug Cheat SheetFast diagnosis commands for common pipeline failures
Build succeeds locally but fails in CI with compilation error
Immediate action
Check Java version mismatch between local and runner
Commands
java -version
grep -r 'java.version\|java.toolchain' pom.xml build.gradle
Fix now
Pin exact JDK version in workflow with java-version: '17' and distribution: 'temurin' in actions/setup-java
Docker push fails with 'no basic auth credentials'+
Immediate action
ECR login token expired or was never acquired in this job
Commands
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
aws sts get-caller-identity
Fix now
Add aws-actions/amazon-ecr-login action step before any docker push. Ensure IAM role has ecr:GetAuthorizationToken permission.
Integration tests pass locally but timeout in GitHub Actions+
Immediate action
Testcontainers can't pull Docker images in CI or runner is resource-constrained
Commands
docker pull postgres:15 && docker images
free -h && nproc
Fix now
Pre-pull test images in a separate step before tests run. Use services: block in GitHub Actions for simple containers instead of Testcontainers when possible.
Secrets showing as '***' in logs but app can't read them+
Immediate action
Secret name mismatch between GitHub Secrets and env: mapping
Commands
env | grep -i db | sed 's/=.*/=<redacted>/'
gh secret list --repo owner/repo
Fix now
Verify exact secret names with gh secret list. Secret names are case-sensitive. Ensure env: block maps DB_PASSWORD: ${{ secrets.DB_PASSWORD }} exactly.
GitHub Actions vs Other CI/CD Platforms for Spring Boot
FeatureGitHub ActionsJenkinsGitLab CICircleCI
Setup time5 minutes (YAML in repo)2-4 hours (server setup)30 minutes (built into GitLab)15 minutes
Spring Boot cachingExcellent (setup-java cache:maven)Manual configurationExcellent (built-in cache)Good (orbs available)
Docker build cachingGitHub Actions Cache backendManual + expensive pluginsRegistry-based cachingDocker Layer Caching (paid)
Secret managementGitHub Secrets + EnvironmentsCredentials pluginCI/CD Variables + VaultContext/environment variables
Matrix buildsNative supportPlugins requiredNative supportNative support
Self-hosted runnersYes (any OS/arch)Yes (master-agent)Yes (GitLab Runner)Yes
Cost modelFree for public, minutes for privateInfrastructure costFree tier + compute unitsCredits-based
Kubernetes deploykubectl/Helm actionsPlugin ecosystemAuto DevOps or manualOrbs available

Key takeaways

1
Structure pipelines as job DAGs (not monolithic jobs) with artifact passing between stages for faster feedback and cleaner re-runs
2
Use AWS OIDC federation to eliminate long-lived credentials
it's a 30-minute setup that removes an entire class of security risk
3
Always add `fetch-depth
0 for SonarQube and sonar.qualitygate.wait=true` to ensure quality gates actually block the build
4
Implement Docker layer caching with Spring Boot's layered JAR feature to reduce typical image build time from 3+ minutes to under 30 seconds
5
Use concurrency groups with `cancel-in-progress
true` for feature branches to avoid wasting runner minutes on superseded commits

Common mistakes to avoid

7 patterns
×

Not caching Maven/Gradle dependencies

Symptom
Every build takes 5-10+ minutes downloading the same artifacts repeatedly
Fix
Add cache: maven to actions/setup-java or use actions/cache with key ${{ hashFiles('**/pom.xml') }}
×

Using latest tag for Docker images in production

Symptom
Rollbacks are impossible because 'latest' is overwritten, and deployments are non-deterministic
Fix
Tag Docker images with the Git SHA (${{ github.sha }}) and pass the exact SHA-tagged image to deployment steps
×

Storing AWS credentials as GitHub Secrets

Symptom
Credentials never get rotated and create a security debt; if leaked, attacker has persistent access
Fix
Use AWS OIDC federation with aws-actions/configure-aws-credentials and role-to-assume. Zero stored credentials required.
×

Running SonarQube without `sonar.qualitygate.wait=true`

Symptom
Analysis uploads but quality gate results are ignored, critical issues ship to production
Fix
Always add -Dsonar.qualitygate.wait=true to the Maven/Gradle SonarQube command so the build fails on gate violations
×

Not setting `fetch-depth: 0` for SonarQube analysis

Symptom
SonarQube blame data is wrong, 'new code' calculation is incorrect, PR decoration shows all code as new
Fix
Add fetch-depth: 0 to actions/checkout in the SonarQube job. Default is 1 (shallow clone).
×

Deploying without waiting for service stability

Symptom
Pipeline shows green while ECS/Kubernetes is still rolling out or has crashed into a restart loop
Fix
Use wait-for-service-stability: true in ECS deploy action or kubectl rollout status --timeout=5m for Kubernetes
×

Using third-party actions pinned to tags instead of SHAs

Symptom
A compromised action author pushes malicious code to a version tag, injecting into your pipeline
Fix
Pin all third-party actions to immutable commit SHAs. Use tools like renovatebot to automate SHA updates.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you design a GitHub Actions pipeline for a Spring Boot microse...
Q02JUNIOR
What is the difference between `actions/cache` and the `cache:` paramete...
Q03SENIOR
Explain how AWS OIDC federation works with GitHub Actions and why it's p...
Q04SENIOR
How do you implement Docker layer caching in GitHub Actions to minimize ...
Q05JUNIOR
What does `fail-fast: false` do in a GitHub Actions matrix strategy, and...
Q06SENIOR
How would you debug a situation where integration tests pass locally but...
Q07SENIOR
Describe the security implications of using `pull_request` vs `pull_requ...
Q08SENIOR
How do you implement a rollback mechanism in your GitHub Actions deploym...
Q01 of 08SENIOR

How would you design a GitHub Actions pipeline for a Spring Boot microservice that needs to deploy to both staging and production with different approval processes?

ANSWER
Use GitHub Environments: create 'staging' and 'production' environments. The staging environment has no protection rules, so every push to main deploys automatically. The production environment has required reviewers configured. In the workflow, the staging deploy job runs after tests pass. The production deploy job uses environment: production and needs: deploy-staging, requiring both the staging deploy to succeed and a manual approval from a configured reviewer. This gives you continuous deployment to staging and controlled deployment to production.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How much does GitHub Actions cost for Spring Boot CI/CD?
02
Should I use self-hosted runners for Spring Boot CI/CD?
03
How do I handle database migrations (Flyway/Liquibase) in my CI/CD pipeline?
04
What's the best way to manage multiple environments (dev/staging/prod) in GitHub Actions?
05
How do I integrate security scanning (OWASP dependency check, Trivy) into the pipeline?
🔥

That's Deployment. Mark it forged?

9 min read · try the examples if you haven't

Previous
Spring Boot Production Deployment Guide
2 / 3 · Deployment
Next
Deployment Rollback Strategies for Spring Boot