> Markdown version of [Build a Container Image](https://vaadin.com/docs/latest/building-apps/production/container). Section index: [llms.txt](https://vaadin.com/docs/latest/building-apps/llms.txt)

# Build a Container Image

A container image packages the JAR from [Build for Production](https://vaadin.com/docs/latest/building-apps/production/build.md) together with a Java runtime. The following `Dockerfile` builds an image from the JAR in the `target` directory:

`Dockerfile`

```dockerfile
FROM eclipse-temurin:25-jre AS builder
WORKDIR /builder
COPY target/*.jar application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

FROM eclipse-temurin:25-jre
RUN useradd --system app
USER app
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "application.jar"]
```

Build the JAR first, then the image, and run it with a memory limit:

```terminal
./mvnw clean package
docker build -t my-app .
docker run -p 8080:8080 -m 2g -e SPRING_PROFILES_ACTIVE=prod my-app
```

## <a id="how-the-dockerfile-works"></a>How the Dockerfile Works

The first stage splits the JAR into layers, as described in the [Spring Boot documentation](https://docs.spring.io/spring-boot/reference/packaging/container-images/dockerfiles.html). The dependencies, which include Vaadin, end up in their own image layer. They change less often than your code, so rebuilding and pulling the image after a code change is faster.

The second stage runs the application as an unprivileged user, and raises the share of memory that the JVM may use for the heap, as explained in the next section. The `ENTRYPOINT` starts Java directly, without a shell in between, so that Java receives the stop signal from the container platform.

## <a id="give-the-jvm-enough-memory"></a>Give the JVM Enough Memory

Unless told otherwise, the JVM uses at most a quarter of the container’s memory for the heap. In a container with 2 GB of memory, the heap gets 512 MB, even when the application needs more, and the container has memory to spare.

A Vaadin application keeps the UI state of every user in the heap, so give the heap most of the memory. With `-XX:MaxRAMPercentage=75`, the same container gets a 1.5 GB heap, and the rest remains for the memory that the JVM needs outside the heap.

Always run the container with a memory limit, such as the `-m` option of `docker run`, or a memory limit in Kubernetes. Without a limit, the JVM sizes its heap according to the memory of the whole host.

How much memory your application needs depends on how many sessions it holds, and on what its views keep in memory. [Measure Memory per Session](https://vaadin.com/docs/latest/building-apps/architecture/capacity-and-failover.md#measure-memory-per-session) shows how to find out.

To pass other JVM options without rebuilding the image, set the `JAVA_TOOL_OPTIONS` environment variable on the container. The JVM reads its options from it.

## <a id="build-inside-docker-instead"></a>Build Inside Docker Instead

Projects from [`start.vaadin.com`](https://start.vaadin.com) come with a `Dockerfile` that runs the Maven build inside Docker, so the machine that builds the image doesn’t need Java. If the application uses commercial features, the build needs a license key, as explained in [When the Build Needs a License Key](https://vaadin.com/docs/latest/building-apps/production/build.md#when-the-build-needs-a-license-key). As the comments in that `Dockerfile` describe, pass the key as a build secret, which isn’t stored in the image:

```terminal
docker build --secret id=offlineKey,src=$HOME/.vaadin/offlineKey -t my-app .
```

This `Dockerfile` runs the application as the `root` user. Consider adding an unprivileged user, as in the example at the top of this page.

If you build the JAR on a CI server anyway, as described in [Build on CI](https://vaadin.com/docs/latest/building-apps/production/ci.md), building the image from that JAR avoids building the application twice.

## <a id="stop-containers-gracefully"></a>Stop Containers Gracefully

When a container platform stops a container, it first sends the application a stop signal, and Spring Boot shuts down gracefully. Open server push connections don’t delay the shutdown. The users on the stopped instance lose their UI state, though. [Test What Users See When a Server Stops](https://vaadin.com/docs/latest/building-apps/architecture/capacity-and-failover.md#test-what-users-see-when-a-server-stops) shows what this looks like, and compares the ways to handle it: accepting the reload, draining the instance, and replicating sessions.

The [Spring Boot documentation](https://docs.spring.io/spring-boot/how-to/deployment/cloud.html#howto.deployment.cloud.kubernetes.container-lifecycle) explains how to delay the shutdown on Kubernetes, so that no new requests are routed to a pod that’s already shutting down.

For more about running Vaadin applications in Docker, see [Docker Deployment](https://vaadin.com/docs/latest/flow/production/docker.md).

## <a id="next-steps"></a>Next Steps

In production, a reverse proxy usually sits in front of the container, and terminates TLS. [Deploy Behind a Proxy](https://vaadin.com/docs/latest/building-apps/production/reverse-proxy.md) shows how to configure it for Vaadin.
