Jib Docker Image Build
1. Overview
EMS services use Google Jib to build container images — no Dockerfile, no Docker daemon. The image is produced by Maven’s package phase when the appropriate profile is active. Jib lays out the image optimally (dependencies in one layer, application classes in another) so incremental builds cache-hit the dependency layer.
Reference base: eclipse-temurin:17-jre-focal. Current target JVM: Java 17 (despite Java 21 in the parent-pom target; Jib image stays on 17 for compatibility with the Alpine-free Focal base). This will move to 21 when eclipse-temurin:21-jre-* stabilises on a Focal-equivalent base.
This page documents the Jib configuration, common operations, and how admin-portal’s image is built.
See also: Maven POM Conventions, Helm Chart Structure.
2. Configuration
From admin-service/pom.xml:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<from>
<image>${jib-maven-plugin.image}</image>
<platforms>
<platform>
<architecture>${jib-maven-plugin.architecture}</architecture>
<os>linux</os>
</platform>
</platforms>
</from>
<to>
<image>docker.io/christhonie/event-admin-service:${project.version}</image>
<auth>
<username>christhonie</username>
<password>${env.DOCKER_HUB_TOKEN}</password>
</auth>
<tags>
<tag>${project.version}</tag>
<tag>latest</tag>
</tags>
</to>
<container>
<mainClass>${main-class}</mainClass>
<jvmFlags>
<jvmFlag>-Djava.security.egd=file:/dev/./urandom</jvmFlag>
<jvmFlag>-Dspring.profiles.active=prod</jvmFlag>
</jvmFlags>
<ports>
<port>${server.port}</port>
</ports>
<labels>
<org.opencontainers.image.title>${project.artifactId}</org.opencontainers.image.title>
<org.opencontainers.image.version>${project.version}</org.opencontainers.image.version>
<org.opencontainers.image.revision>${git.commit.id.full}</org.opencontainers.image.revision>
</labels>
</container>
<extraDirectories>
<paths>
<path>
<from>src/main/jib</from>
</path>
</paths>
</extraDirectories>
</configuration>
</plugin>
Properties from the parent POM or service POM:
-
jib-maven-plugin.image=eclipse-temurin:17-jre-focal -
jib-maven-plugin.architecture=amd64(linux/amd64; crossbuild to arm64 via explicit override if needed) -
main-class=za.co.idealogic.event.admin.AdminServiceApp(per service)
3. Build Commands
Build to local Docker daemon (for dev iteration with a local Kubernetes):
./mvnw -Pprod package jib:dockerBuild
Build and push directly to registry (CI, no daemon required):
./mvnw -Pprod package jib:build
Build to tarball (offline, sneakernet):
./mvnw -Pprod package jib:buildTar
# produces target/jib-image.tar
Push the tarball separately with docker load + docker push or skopeo copy tarball:….
4. Layering
Jib lays out four layers (bottom to top):
-
Base image (
eclipse-temurin:17-jre-focal) — shared across all EMS services -
Dependencies — the JARs under
WEB-INF/libfor the application; changes rarely, big layer -
Resources —
src/main/resources/content (config, Liquibase changelogs, i18n) -
Classes — compiled application classes + static webapp output
A pure source change touches only layer 4; dependency changes touch layer 2 and bust downstream cache. Incremental builds cache-hit layers 1-3 typically.
This layering is important for registry + pull performance: small diffs on patches keep rollout fast.
5. src/main/jib/ Extra Directory
Everything placed in a directory referenced by Jib’s <extraDirectories> is copied into the image’s root filesystem at the corresponding path. Common uses:
-
Custom
/etc/ssl/certs/additions if the service needs to trust private CAs -
OpenTelemetry javaagent download (see § OTel javaagent below)
-
Non-Maven static files that belong in the image but not on the classpath
Keep contents minimal — every file here inflates the image.
5.1. OTel javaagent — the EMS pattern
The OTel javaagent is always baked into every EMS service image, regardless of which Spring profile is active. Whether the agent actually exports data depends on runtime config (the otlp profile + endpoint setting), but the agent itself is always present so the image is consistent across environments.
The pattern is two cooperating Maven plugin executions, both in the main <build> section (not in any profile):
5.1.1. 1. Maven downloads the agent into the build output
<properties>
<agent-extraction-root>${project.build.directory}/jib-agents</agent-extraction-root>
<agent-install-location>/javaagent</agent-install-location>
<opentelemetry-javaagent-filename>opentelemetry-javaagent.jar</opentelemetry-javaagent-filename>
<opentelemetry-javaagent.version>2.10.0</opentelemetry-javaagent.version>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-javaagent</id>
<phase>package</phase>
<goals><goal>copy</goal></goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.opentelemetry.javaagent</groupId>
<artifactId>opentelemetry-javaagent</artifactId>
<version>${opentelemetry-javaagent.version}</version>
<outputDirectory>${agent-extraction-root}</outputDirectory>
<destFileName>${opentelemetry-javaagent-filename}</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
Output: target/jib-agents/opentelemetry-javaagent.jar.
5.1.2. 2. Jib copies it into the image and wires the JVM flag
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<extraDirectories>
<paths>
<path>
<from>${agent-extraction-root}</from>
<into>${agent-install-location}</into>
</path>
</paths>
</extraDirectories>
<container>
<environment>
<OTEL_SERVICE_NAME>${project.artifactId}</OTEL_SERVICE_NAME>
</environment>
<jvmFlags>
<jvmFlag>-javaagent:${agent-install-location}/${opentelemetry-javaagent-filename}</jvmFlag>
<jvmFlag>-Dotel.logs.exporter=otlp</jvmFlag>
<jvmFlag>-Dotel.traces.exporter=otlp</jvmFlag>
<jvmFlag>-Dotel.metrics.exporter=otlp</jvmFlag>
<!-- ... other JVM flags ... -->
</jvmFlags>
</container>
</configuration>
</plugin>
In the running container, the agent is at /javaagent/opentelemetry-javaagent.jar and the JVM is started with -javaagent:/javaagent/opentelemetry-javaagent.jar. The exporter selectors (-Dotel.{logs,traces,metrics}.exporter=otlp) are explicit because the agent’s default behaviour for some signal types varies across versions.
5.1.3. Spring-side instrumentation lives in the otlp profile
The otlp Maven profile adds the Spring-side dependencies that change runtime behaviour by their presence — opentelemetry-spring-boot-starter and opentelemetry-instrumentation-annotations — and activates application-otlp.yml.
opentelemetry-logback-appender-1.0 is the exception and belongs in the main <dependencies>, not the profile: OpenTelemetryLoggingConfiguration lives in src/main/java and references it unconditionally, so confining it to a profile breaks a plain mvn verify. This was learned the hard way in admin-service (ADO-1009).
The profile must also be activated in CI. push-main.yml and push-dev.yml pass it through maven-profiles; a profile that exists and is never activated leaves application-otlp.yml with no consumer, which is exactly what happened in registration-portal’s first cut of ADO-1010. It which configures the exporter endpoint and disables Spring Web auto-instrumentation (the agent already covers it). See OpenTelemetry Configuration for the full Spring config.
5.1.4. Anti-patterns to avoid
| Mistake | What goes wrong |
|---|---|
Maven downloads to |
Agent silently absent from the image. JVM logs |
Maven downloads to |
Pollutes source-control state. Agent jars belong in |
Putting the |
Image only has the agent when the otlp profile was active at build time. Different image content per profile-combo. Rebuild matrix grows. Always download; gate the use of the agent at runtime via Spring profile + env vars. |
Using a custom |
The Jib |
Setting |
Jib ignores |
Hard-coding the agent path in a shell script instead of interpolating the Maven properties |
The script literal and |
5.2. No custom entrypoint
EMS services do not set Jib’s <entrypoint>. Jib generates one from the Spring Boot extension, computing the layered classpath itself, and <jvmFlags> are honoured only in that mode.
This matters beyond tidiness. registration-portal carried a JHipster-generated entrypoint.sh until ADO-1010, and it was doing three things — none of which Kubernetes needed:
| What the script did | Why it was unnecessary |
|---|---|
|
|
|
The Helm chart injects every one of them through |
|
Exactly what Jib’s Spring Boot extension computes on its own, and it produces |
Removing it is what allows the agent flag to live in the POM at all.
5.3. Verify the agent against the artefact, not the source
Every failure mode here is silent: the build succeeds, the image publishes, and the service runs with no instrumentation. ems-mcp-server shipped that way for four months. Reading the POM is not evidence — unpack the image and look:
mvn -o package -DskipTests jib:buildTar -Djib.skip=false
mkdir -p /tmp/img && tar -xf target/jib-image.tar -C /tmp/img && cd /tmp/img
# 1. Is the agent in a layer, at the expected path?
for f in *.tar.gz; do tar -tzf "$f" | grep -q javaagent && \
echo "$f: $(tar -tzf "$f" | grep javaagent | tr '\n' ' ')"; done
# 2. Does the generated entrypoint actually carry the flag?
python3 -c "import json; print(json.load(open('config.json'))['config']['Entrypoint'])"
A correct result looks like this — note that the flag appears in the entrypoint, which is the proof <jvmFlags> were honoured:
java -javaagent:/javaagent/opentelemetry-javaagent.jar
-Dotel.instrumentation.logback-appender.enabled=false
-noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom
-cp @/app/jib-classpath-file za.co.idealogic...App
|
Until 2026-08-24 the shared The workflow now runs |
6. Registry Authentication
In CI (GitHub Actions), DOCKER_HUB_TOKEN is set as a repository secret and passed via env:
- name: Build + push image
run: ./mvnw -Pprod package jib:build
env:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
Locally: run docker login once; Jib picks up the credentials from ~/.docker/config.json.
For Kubernetes pull: image pull secret named christhonie-docker is deployed in every namespace that pulls EMS images.
7. Tagging Strategy
Primary tag: <version> from project.version. For releases: e.g. 2.3.31-RELEASE. For snapshots: 2.3.32-SNAPSHOT.
Floating tag: latest. Points to the most recent push on any branch — useful for local dev iteration, never referenced in Helm values for prod (always pin to a version).
CI-only tag: <branch>-<shortsha> on feature branches, for preview deployments. Not currently automated; add per need.
8. Admin-Portal Specifics
admin-portal’s Jib config mirrors registration-portal’s, with obvious adjustments:
-
<image>target:docker.io/christhonie/event-admin-portal:${project.version} -
<mainClass>→za.co.idealogic.event.admin.portal.AdminPortalApp -
<port>→12506 -
<jvmFlag>-Dspring.profiles.active=prod,kubernetes</jvmFlag>default for the prod container -
OTel javaagent: optional at launch — admin-portal is not as request-heavy as admin-service; add later if needed.
9. Image Size and Startup
Target image size (admin-service): ~400 MB. Dominated by the base image (~200 MB for temurin-17-jre-focal) + dependencies (~150 MB of Spring + Hibernate + Hazelcast) + app classes + extras.
Cold start: Spring context-up is 15-25 seconds depending on Liquibase migration count. Portals (no Liquibase-heavy paths) start faster — 10-15s typical.
If startup becomes a bottleneck, CRaC (Coordinated Restore at Checkpoint) or native-image (GraalVM) are options; neither is currently in use. Defer until there’s a concrete operational need.
10. Troubleshooting
10.1. jib:build fails with Unauthenticated to Docker Hub
DOCKER_HUB_TOKEN is missing or expired. Regenerate on Docker Hub; update the GitHub secret or local env.
10.2. Image pulls but container crashes on start
Check kubectl logs — most common cause is missing application-*.yml or a required env var not mounted. See Helm Chart Structure for the values the chart injects.
11. Reference
| File | Role |
|---|---|
|
Full Jib config reference (inherits some from parent-pom) |
|
Extra directory — includes OTel javaagent in |
|
Portal-shaped Jib config (frontend-maven-plugin integration) |
|
Jib plugin version pin, default base image property |
13. Change History
| Date | Change |
|---|---|
2026-04-24 |
Initial draft. Grounded in admin-service Jib configuration. |
2026-08-24 |
ADO-1010. Recorded that Jib ignores |