Skaffold with Jib lets you build container images for your Java applications directly from your build tool, bypassing the need for a Docker daemon and Dockerfile.
Here’s a typical workflow:
Let’s say you have a Spring Boot application. You’d typically build a JAR, then use a Dockerfile to copy it into an image. With Jib, you integrate it into your Maven or Gradle build.
<!-- Maven pom.xml -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version> <!-- Use the latest version -->
<configuration>
<to>
<image>my-dockerhub-username/my-app:${project.version}</image>
</to>
</configuration>
</plugin>
// Gradle build.gradle
plugins {
id 'com.google.cloud.tools.jib' version '3.4.0' // Use the latest version
}
jib {
to {
image = 'my-dockerhub-username/my-app:${project.version}'
}
}
When you run mvn compile jib:build or gradle jibBuild, Jib analyzes your project, identifies dependencies, compiles your code, and packages everything into a container image. It intelligently layers your application, so only changed code invalidates the cache, making subsequent builds much faster.
Skaffold then hooks into this. You configure skaffold.yaml to use Jib as your builder.
apiVersion: skaffold/v2
kind: Config
build:
artifacts:
my-app:
jib: {} # This tells Skaffold to use Jib
local:
push: false # For local development, push to a local registry or skip pushing
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
Now, when you run skaffold dev, Skaffold will:
- Trigger Jib to build your container image.
- Tag the image appropriately.
- Load the image into your local Docker daemon (or push it if configured).
- Apply your Kubernetes manifests.
- Watch for changes in your code. When changes are detected, it will rebuild the image using Jib and redeploy your application to Kubernetes.
This eliminates the need to write and maintain Dockerfiles for simple Java applications, speeds up build times due to Jib’s caching, and streamlines the inner development loop significantly. Jib handles the complexities of creating an efficient container image—like separating dependencies from application code—automatically.
The most surprising thing about Jib is how it achieves efficient layering without a Dockerfile. It achieves this by analyzing your project’s build output and dependency structure. Instead of a single COPY command for your entire application, Jib creates separate layers for dependencies, resources, and compiled classes. This granular layering is key to its speed and caching efficiency. When you change just your source code, Jib only rebuilds and re-uploads the layer containing your compiled classes, leaving the dependency layers untouched.
Consider this skaffold.yaml snippet:
apiVersion: skaffold/v2
kind: Config
build:
artifacts:
my-app:
jib: {}
local:
push: false
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
profiles:
- name: dev
build:
artifacts:
my-app:
jib:
args:
- --build-args
- MODULES=my-app-core,my-app-web
# ... other dev-specific configurations
Here, jib: {} is the minimal configuration. Skaffold defaults to calling mvn compile jib:build or gradle jibBuild. You can pass specific arguments to Jib using args. For instance, if your project has multiple modules and you want Jib to build only specific ones, you’d add them to the jib configuration. This allows fine-grained control over the Jib build process directly within Skaffold.
The local.push: false setting is crucial for local development. It tells Skaffold not to push the built image to a remote registry. Instead, it will load the image directly into your local Docker daemon’s image store. This is much faster than pushing and pulling from a registry during rapid iteration. If you were using a remote registry for development, you’d set local.push: true and configure the registry in the jib section of your skaffold.yaml.
The real power comes from skaffold dev. When you run it, Skaffold watches your source files. If you modify a Java file, Skaffold detects the change, tells Jib to rebuild the image (which is fast due to Jib’s layer caching), and then updates your Kubernetes deployment with the new image. This creates a seamless inner development loop where you can see your code changes reflected in your running Kubernetes cluster within seconds, without manual docker build or docker push commands.
The jib.from configuration within skaffold.yaml allows you to specify the base image Jib should use. For example:
apiVersion: skaffold/v2
kind: Config
build:
artifacts:
my-app:
jib:
from: "openjdk:17-jdk-slim" # Specify a custom base image
local:
push: false
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
This level of customization ensures you have control over the underlying runtime environment of your container, even when abstracting away Dockerfiles.
One common pitfall is not understanding how Jib handles multi-module Maven/Gradle projects. By default, Jib might try to build all modules. If you only want to build and containerize a specific web module, you need to explicitly tell Jib which module’s output to package. This is often done by configuring the Jib plugin within the specific module’s pom.xml or build.gradle, or by passing module names via the args in skaffold.yaml as shown earlier. Without this, you might end up with a large image containing unnecessary build artifacts from other modules.
The next step is exploring how Skaffold can manage multi-container applications and more complex deployment strategies with Jib as the builder.