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 only adds the Spring-side dependencies (opentelemetry-spring-boot-starter, opentelemetry-instrumentation-annotations, opentelemetry-logback-appender-1.0) and activates application-otlp.yml 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 |
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 |