Docker & OCI images
Curie builds container images the Jib way by default: pull a base image from a registry, append deterministic dependency and app layers, write an OCI layout — no Docker daemon required to assemble the image. You can still fall back to docker build when you want a classic Dockerfile workflow.
When it runs
curie build on an [application] project always produces the JAR. When Docker is in scope — a [docker] section in Curie.toml, or a project-root Dockerfile — Curie also builds a container image. Library projects never produce images.
Builders
Two assembly strategies:
| Builder | When | Needs Docker daemon? | Output |
|---|---|---|---|
daemonless (default) |
Default whenever [docker] is present and there is no project-root Dockerfile |
No (for build) | target/image/ (OCI layout) + target/image.tar |
docker |
builder = "docker" in Curie.toml, or a project-root Dockerfile exists |
Yes | Image loaded into the local Docker daemon |
A project-root Dockerfile always forces the Docker CLI path — the daemonless builder cannot process Dockerfiles.
Configuration
[application] name = "greeter" version = "0.1.0" mainClass = "com.example.Greeter" [java] releaseVersion = "21" # match the JRE in baseImage [docker] baseImage = "eclipse-temurin:21-jre-alpine" jvmArgs = ["-Xmx512m"] env = { TZ = "UTC" } labels = { "org.opencontainers.image.source" = "https://github.com/acme/greeter" }
| Key | Default | Notes |
|---|---|---|
builder | daemonless | daemonless or docker (alias daemon) |
baseImage | JRE alpine for JARs; debian:trixie-slim for native/jlink | Tag or name@sha256:… digest pin |
imageName / imageTag | app name / version | Full ref is name:tag |
platform | linux/amd64 | Selected from multi-arch base indexes (daemonless) |
jvmArgs | [] | Spliced into java <args> -jar app.jar (daemonless) |
env / labels / user | empty | Image config metadata (daemonless) |
registryId | — | Matches a [[credentials]] entry for private registries |
tags | [] | Extra tags (reserved for push) |
Match [java] releaseVersion to the JRE inside baseImage — a class-file version newer than the container JRE will fail at runtime.
Daemonless layers (default)
Like Google's Jib, Curie stacks layers so the large, stable dependency set rarely invalidates:
- Base image layers (pulled by digest, cached under
~/.curie/oci/blobs/) /app/libs/*— dependency JARs (same collision-safe names as the JARClass-Path)/app/app.jar— the thin application JAR
Entrypoint: ["java", <jvmArgs…>, "-jar", "app.jar"], working directory /app. Layers use fixed timestamps (2024-01-01 UTC) so identical inputs produce identical digests — rebuilds that change nothing are content-addressed no-ops for the new layers.
Outputs:
target/image/— OCI image layout (oci-layout,index.json,blobs/)target/image.tar— loadable archive (docker load -i target/image.tar)
Package greeter-0.1.0.jar OCI image pulling base eclipse-temurin:21-jre-alpine OCI image greeter:0.1.0 (target/image.tar, sha256:…)
Registry auth
Public base images (Docker Hub library images, etc.) work anonymously. For private registries, credentials are resolved in this order:
CURIE_REGISTRY_USERNAME/CURIE_REGISTRY_PASSWORD[[credentials]]in~/.curie/config.tomlmatched by[docker] registryId~/.docker/config.json(auths,credHelpers,credsStore— sodocker loginjust works)
Offline
With --offline, the base image manifest, config, and layers must already be in ~/.curie/oci/ from a previous online pull; any miss is a hard error.
Docker CLI builder
Set builder = "docker" (or place a Dockerfile at the project root) to shell out to docker build. Curie generates a layer-friendly Dockerfile when you don't supply one:
FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY libs/ libs/ COPY greeter-0.1.0.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]
The JAR's Class-Path manifest entry points at libs/, so java -jar app.jar finds every transitive dep. A generated .dockerignore keeps the build context small.
Daemonless-only fields (jvmArgs, env, labels, user, platform, tags, registryId) are rejected when builder = "docker" — remove them or switch back to the default.
Running
curie run builds the image and launches a container. With the daemonless builder it loads target/image.tar via docker load first, then docker run. Pass --no-docker to invoke java -jar on the host instead.
Package up to date
OCI image greeter:0.1.0 (target/image.tar, sha256:…)
Docker load target/image.tar
Run greeter:0.1.0
Hello from greeter.
Or load manually: docker load -i target/image.tar && docker run --rm greeter:0.1.0.
Fat JARs
When [fat-jar] is enabled the image is base + one fat layer (no separate deps layer). Prefer the default thin JAR for containers so dependency layers stay cacheable.
Docker + native / jlink
When [native-image] or [jlink] is also enabled, the generated Dockerfile packages that artifact instead of the plain JAR — neither needs a JRE, so the default base image switches from eclipse-temurin:21-jre-alpine to debian:trixie-slim (still overridable via baseImage). If both sections are present, [native-image] wins.
With [native-image], the binary is copied in directly and run without a JVM at all:
FROM debian:trixie-slim WORKDIR /app COPY greeter ./greeter ENTRYPOINT ["./greeter"]
With [jlink], the whole target/runtime/ tree (the custom JDK plus the app JAR and its libs/) is copied in, and the launcher script is the entrypoint — no java -jar and no separate JRE base image, since the runtime image already bundles its own JDK:
FROM debian:trixie-slim WORKDIR /app COPY runtime/ runtime/ ENTRYPOINT ["runtime/bin/greeter"]
Both curie build and curie run build the native/jlink artifact first when Docker is going to package it — if you skip that step explicitly (--no-native / --no-jlink) on a fresh checkout with nothing built yet, Docker fails with an actionable error rather than silently falling back to the JAR.
Incremental Docker builds
The Dockerfile + .dockerignore are stamped against their inputs: the JAR mtime, the libs/ directory mtime, and Curie.toml's [docker] table (or, in native/jlink mode, the binary's or the runtime/ tree's mtime instead). If none of those moved since the last image build, Curie skips the docker build invocation entirely.