Spring Boot Tutorial
CI/CD for Spring Boot with GitHub Actions
Continuous integration means every push is automatically built and tested; continuous delivery means every passing build produces a deployable artifact — and, ideally, is deployed automatically. For a Spring Boot team, a good pipeline catches broken tests, vulnerable dependencies and failed migrations minutes after a push instead of days later in production.
This lesson builds a complete GitHub Actions pipeline for a Spring Boot project: build and test with Maven caching and Testcontainers, publish test reports, scan dependencies, build and push a container image, and deploy to Kubernetes with an approval step for production.
Pipeline Stages
A typical pipeline runs: build and unit tests on every push and pull request; integration tests with Testcontainers (GitHub-hosted Linux runners include Docker); quality and security checks — dependency vulnerability scanning, static analysis, code coverage; package a container image tagged with the commit SHA; and deploy to staging automatically and to production after approval.
Speed Matters
A slow pipeline gets ignored. Cache the Maven or Gradle repository (actions/setup-java has built-in caching), run independent jobs in parallel, reuse the Spring test context across tests, and share Testcontainers containers. Aim for feedback on pull requests within 10 minutes.
Secrets and Environments
Store registry tokens, cloud credentials and kubeconfig in GitHub secrets, never in the repository. Use GitHub environments ("staging", "production") with protection rules such as required reviewers, so production deployments need an explicit approval. Prefer short-lived OIDC credentials for cloud providers over long-lived keys.
Examples
Build and test on every push and pull request
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Build and test (unit + Testcontainers integration tests)
run: ./mvnw -B verify
- name: Publish test report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: target/surefire-reports/
✓ build (6m 12s)
Tests run: 412, Failures: 0, Errors: 0, Skipped: 3
Containers started: postgres:17, redis:8, apache/kafka:4.1.0
Security scanning and building/pushing the image
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '21', cache: maven }
- name: Dependency vulnerability scan
run: ./mvnw -B org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7
image:
needs: [build, security]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '21', cache: maven }
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image with buildpacks and push
run: |
IMAGE=ghcr.io/${{ github.repository }}:${{ github.sha }}
./mvnw -B -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=$IMAGE
docker push $IMAGE
✓ security (3m 40s) — no vulnerabilities with CVSS >= 7
✓ image (4m 05s) — pushed ghcr.io/webnest/shop:3f9c2e1
Deploying to staging automatically and production with approval
deploy-staging:
needs: image
runs-on: ubuntu-latest
environment: staging
steps:
- uses: azure/setup-kubectl@v4
- name: Deploy
env:
KUBECONFIG_DATA: ${{ secrets.STAGING_KUBECONFIG }}
run: |
echo "$KUBECONFIG_DATA" | base64 -d > kubeconfig
kubectl --kubeconfig kubeconfig set image deployment/webnest-shop app=ghcr.io/${{ github.repository }}:${{ github.sha }}
kubectl --kubeconfig kubeconfig rollout status deployment/webnest-shop --timeout=5m
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # requires reviewer approval in repository settings
steps:
- uses: azure/setup-kubectl@v4
- name: Deploy
env:
KUBECONFIG_DATA: ${{ secrets.PROD_KUBECONFIG }}
run: |
echo "$KUBECONFIG_DATA" | base64 -d > kubeconfig
kubectl --kubeconfig kubeconfig set image deployment/webnest-shop app=ghcr.io/${{ github.repository }}:${{ github.sha }}
kubectl --kubeconfig kubeconfig rollout status deployment/webnest-shop --timeout=5m
✓ deploy-staging (1m 20s)
⏸ deploy-production waiting for approval from @webnest/leads
✓ deploy-production (1m 31s) — approved by vansh
Common Mistakes
- Skipping integration tests in CI because "Docker is complicated" — GitHub-hosted runners support Testcontainers.
- Tagging images only with latest, making it impossible to know which commit is running.
- Storing credentials in workflow files instead of GitHub secrets.
- Deploying to production automatically with no approval or smoke test.
- Letting the pipeline grow to 40+ minutes, so developers stop waiting for it.
Key Points to Remember
- Build and test every push and pull request; include Testcontainers integration tests.
- Cache dependencies and parallelise jobs for fast feedback.
- Scan dependencies and fail on serious vulnerabilities.
- Build images tagged with the commit SHA and push to a registry.
- Deploy to staging automatically and to production through a protected environment.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.