Build & Docker Image
Overview
The backend is compiled using Maven, packaged as a Spring Boot JAR, and then containerized using a multi-stage Docker build for optimization and security.
Source Code → Maven Compile → Tests → JAR Package → Docker Image → Registry
This document covers: - Maven build pipeline configuration - Dockerfile structure and multi-stage strategy - Container entrypoint and startup - Docker image validation
Maven Build Pipeline
Project Configuration:
pom.xml
The pom.xml defines the complete build
process:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>inventory-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- Spring Boot parent: includes plugins for build, test, Docker -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<!-- Dependencies: Spring, OAuth2, Oracle, TestContainers, etc. -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Oracle JDBC -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
</dependency>
<!-- Test dependencies -->
</dependencies>
<!-- Build plugins -->
<build>
<plugins>
<!-- Spring Boot Maven Plugin: builds fat JAR with embedded Tomcat -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- JaCoCo: test coverage instrumentation -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Build Lifecycle Stages
When you run mvn clean verify:
| Stage | Command | What Happens | Output |
|---|---|---|---|
| Clean | clean |
Deletes previous build artifacts from
/target |
Clean slate |
| Compile | compile |
Compiles source code to bytecode | .class files in
/target/classes |
| Test Compile | test-compile |
Compiles test code | Test .class files in
/target/test-classes |
| Test | test |
Runs unit tests (JUnit, Mockito) | Test results, code coverage (JaCoCo) |
| Integration Test | integration-test |
Runs integration tests (TestContainers, @SpringBootTest) | Live database tests |
| Package | package |
Builds JAR file with all dependencies | /target/inventory-service-0.0.1-SNAPSHOT.jar |
| Verify | verify |
Runs post-integration-test checks (coverage thresholds, etc.) | Build success/failure |
Maven Profiles
Spring profiles control which
application-{profile}.yml gets activated:
<profiles>
<!-- Local development (default, no profile specified) -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!-- Testing profile: H2 in-memory, debug logging -->
<profile>
<id>test</id>
<properties>
<maven.test.skip>false</maven.test.skip>
</properties>
</profile>
<!-- Production profile: Oracle, INFO logging -->
<profile>
<id>prod</id>
</profile>
</profiles>Activate via: -Dspring.profiles.active=prod or
SPRING_PROFILES_ACTIVE environment variable.
Dependency Management
Key dependencies controlled in pom.xml:
Spring Framework (Web, Data, Security)
├── spring-boot-starter-web (REST APIs, Tomcat)
├── spring-boot-starter-data-jpa (Entity management)
├── spring-boot-starter-security (Authentication/authorization)
└── spring-security-oauth2-client (Google SSO)
Data & Persistence
├── spring-boot-starter-data-jpa (ORM layer)
├── org.hibernate (JPA implementation)
└── oracle.database.jdbc (JDBC driver)
Testing
├── spring-boot-starter-test (JUnit, Mockito, AssertJ)
├── testcontainers (Docker-based integration tests)
└── com.h2database (In-memory testing database)
Build Tools
├── spring-boot-maven-plugin (Fat JAR, Docker support)
├── jacoco-maven-plugin (Code coverage)
└── maven-shade-plugin (Optional: fat JAR creation)
Note: Oracle JDBC driver
(ojdbc11.jar) is bundled in the Docker image and
also available at /lib/ojdbc11.jar in the
repository.
Docker Build: Multi-Stage Approach
Dockerfile Structure
# ============================================================================
# STAGE 1: BUILD (Compile Maven project)
# ============================================================================
FROM eclipse-temurin:17-jdk-jammy as builder
WORKDIR /app
# Copy Maven wrapper and pom.xml
COPY mvnw .
COPY mvnw.cmd .
COPY .mvn .mvn
COPY pom.xml .
# Build the application
# ARG PROFILE=prod allows override: docker build --build-arg PROFILE=dev
ARG PROFILE=prod
RUN chmod +x mvnw && \
./mvnw clean package -DskipTests -Dspring.profiles.active=${PROFILE}
# ============================================================================
# STAGE 2: RUNTIME (Minimal image with JAR only)
# ============================================================================
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
# Copy compiled JAR from builder stage (not entire /target directory)
COPY --from=builder /app/target/inventory-service-*.jar app.jar
# Copy start script and scripts
COPY start.sh .
RUN chmod +x start.sh
# Copy Oracle Wallet (mounted at runtime via volumes/env)
COPY oracle_wallet oracle_wallet
# Expose port (8080 or 8081 depending on config)
EXPOSE 8080
# Health check: curl /health endpoint every 30 seconds
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Entrypoint: Start the application
ENTRYPOINT ["./start.sh"]Why Multi-Stage?
Stage 1 (Builder): - Contains Maven, source code, test dependencies - Compiles code, runs tests, produces JAR - Size: ~1.2 GB (Java SDK + Maven + dependencies) - Discarded after build – not in final image
Stage 2 (Runtime): - Contains only Java Runtime (JRE, not JDK) - Copies JAR from Stage 1 - Size: ~400 MB (JRE + JAR only) - This is deployed to production
Benefit: Final image is ~70% smaller, reducing deployment time and storage.
Build Arguments
# Build with prod profile (default)
docker build -t inventory-service:latest .
# Build with dev profile (for local testing)
docker build --build-arg PROFILE=dev -t inventory-service:dev .
# Build with custom tag
docker build --build-arg PROFILE=prod -t ckbuzin/inventory-service:a1b2c3d4 .Container Entrypoint:
start.sh
The start.sh script launches the Spring Boot
application with environment variables:
#!/bin/bash
set -e
echo "Starting Smart Supply Pro Inventory Service..."
echo "Profile: ${SPRING_PROFILES_ACTIVE:-prod}"
echo "Database: ${DB_URL:-oracle}"
# Export environment variables for Spring Boot
export SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-prod}
export TNS_ADMIN=${TNS_ADMIN:-/app/oracle_wallet}
export JAVA_OPTS="-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -Xmx512m -Xms256m"
# Start the application
exec java -jar app.jarKey aspects:
- Profile selection: Defaults to
prodifSPRING_PROFILES_ACTIVEnot set - Oracle Wallet path: Set to
/app/oracle_wallet(mounted from Fly.io secrets) - JVM memory: Tuned for container constraints (256-512 MB heap)
- exec keyword: Replace shell process with Java (proper signal handling)
Image Contents Verification
After Docker build, verify the image contains what we expect:
# Inspect image layers and size
docker history ckbuzin/inventory-service:latest
# Check what's inside
docker run --rm ckbuzin/inventory-service:latest sh -c "
echo '=== App JAR ===' && ls -lh /app/app.jar
echo '=== Start Script ===' && ls -lh /app/start.sh
echo '=== Oracle Wallet ===' && ls /app/oracle_wallet
echo '=== No Source Code ===' && ls /app/src 2>&1 || echo 'Confirmed: src/ not in image'
echo '=== No Test Files ===' && ls /app/target 2>&1 || echo 'Confirmed: target/ not in image'
"Expected output:
=== App JAR ===
-rw-r--r-- 1 root root 82M Nov 20 10:30 /app/app.jar
=== Start Script ===
-rwxr-xr-x 1 root root 612B Nov 20 10:30 /app/start.sh
=== No Source Code ===
Confirmed: src/ not in image
=== No Test Files ===
Confirmed: target/ not in image
This proves the image is production-optimized and doesn’t contain unnecessary files.
Docker Build in CI Pipeline
In 1-ci-test.yml, the Docker build step:
- name: Build Docker image
run: |
docker build --no-cache \
--build-arg PROFILE=prod \
-t $IMAGE_REPO:latest \
-t $IMAGE_REPO:${IMAGE_TAG_SHA} \
.Key flags:
--no-cache– Rebuild all layers (ensures fresh build, no stale cache)--build-arg PROFILE=prod– Pass prod profile to Dockerfile-t TAG1 -t TAG2– Tag with bothlatestand commit SHA
After build, the image is: 1. Inspected for contents 2. Pushed to Docker Hub 3. Scanned by Trivy for vulnerabilities
Troubleshooting
Build Fails: “Maven command not found”
- Ensure
mvnw(Maven wrapper) is executable and in repo root - Check
pom.xmlsyntax (XML well-formed) - Review dependency resolution:
mvn dependency:tree
Docker Build Fails: “Dockerfile not found”
- Verify
Dockerfileexists in repo root or BACKEND_DIR - Check file is named exactly
Dockerfile(case-sensitive on Linux) - Ensure it’s not in
.dockerignore
Image Push Fails: “Unauthorized”
- Verify Docker Hub credentials in
DOCKER_USERNAME,DOCKER_PASSWORDsecrets - Check user has push permissions to repository
- Ensure image tag matches format:
username/repo:tag
Image Size Too Large
- Check
docker historyto find bloated layers - Ensure multi-stage build is working (Stage 1 should be discarded)
- Remove unnecessary dependencies from
pom.xml - Clean up cached Maven dependencies:
.dockerignoreshould include.m2
Trivy Scan Blocks Deployment
- Review scan report for HIGH/CRITICAL CVEs
- Update vulnerable dependencies in
pom.xml - Run locally:
trivy image ckbuzin/inventory-service:latest - See CI/CD & Docs Pipeline
Related Documentation
- Deployment Index – Overview of entire deployment pipeline
- Fly.io Infrastructure – How image is used in production
- CI/CD & Docs Pipeline – GitHub Actions build automation
- Resources & Configuration – Spring Boot configuration files
Last Updated: November 2025
Audience: Backend developers, DevOps engineers,
build system maintainers