> Markdown version of [Build on CI](https://vaadin.com/docs/latest/building-apps/production/ci). Section index: [llms.txt](https://vaadin.com/docs/latest/building-apps/llms.txt)

# Build on CI

A continuous integration (CI) server builds and tests the application on every change, and produces the artifact that you deploy. The following GitHub Actions workflow builds the application, runs its tests, creates the production JAR, and builds a container image from it:

`.github/workflows/build.yml`

```yaml
name: Build

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: 25
          cache: maven

      - name: Build and test
        run: ./mvnw -B verify
        env:
          VAADIN_OFFLINE_KEY: ${{ secrets.VAADIN_OFFLINE_KEY }}

      - name: Build container image
        run: docker build -t my-app:${{ github.sha }} .
```

Other CI servers need the same steps: a JDK, the Maven wrapper that comes with the project, and Docker.

## <a id="what-the-workflow-does"></a>What the Workflow Does

The workflow runs for every push to the `main` branch, and for every pull request:

1. It checks out the code, and installs Java. Use the version that the `java.version` property in your `pom.xml` specifies. The `cache` option keeps the Maven dependencies between runs, so they aren’t downloaded every time.

2. `./mvnw -B verify` compiles the application, runs the tests, and creates the production JAR, as described in [Build for Production](https://vaadin.com/docs/latest/building-apps/production/build.md). If a test fails, the workflow stops. The [Testing](https://vaadin.com/docs/latest/building-apps/testing.md) guides show how to write tests for your views that run without a browser, and therefore also on a CI server.

3. It builds the container image with the `Dockerfile` from [Build a Container Image](https://vaadin.com/docs/latest/building-apps/production/container.md).

To deploy the image, push it to the container registry that your platform uses. How to log in and push depends on the registry.

## <a id="provide-the-license-key"></a>Provide the License Key

If the application uses commercial features, the production build needs a license key, and fails without one. The CI server needs a server license key, which you can download from [your Vaadin account](https://vaadin.com/myaccount/licenses). See [Validation on CI/CD or Build Server](https://vaadin.com/docs/latest/flow/configuration/licenses.md#server-license-key) for details.

Don’t commit the key. Store the contents of the key file as a secret in the CI server instead. In GitHub, add it as a repository secret named `VAADIN_OFFLINE_KEY`. The workflow passes it to the build in the `VAADIN_OFFLINE_KEY` environment variable, where Vaadin looks for it.

If the application doesn’t use commercial features, the build doesn’t need a key. The variable is then empty, which does no harm.

GitHub doesn’t give secrets to workflows that run for pull requests from forks of the repository. If your application uses commercial features, the production build of such pull requests fails.

## <a id="keep-the-frontend-build-fast"></a>Keep the Frontend Build Fast

If the application uses only Vaadin’s UI components and themes, the build uses a pre-compiled frontend bundle, and the CI server doesn’t need Node.js. If the application has an application-specific bundle, commit `src/main/bundles/prod.bundle`, as described in [When the Build Needs Node.js](https://vaadin.com/docs/latest/building-apps/production/build.md#when-the-build-needs-node-js). The CI build then reuses it, and runs npm and Vite only when the frontend code has changed.

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

You now have a production JAR, a container image, and a build that creates them on every change. To run more than one instance of the application, see [Running More Than One Instance](https://vaadin.com/docs/latest/building-apps/production.md#running-more-than-one-instance).
