> For the complete documentation index, see [llms.txt](https://docs.sealsecurity.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sealsecurity.io/setup-apps-os/seal-os-cicd/container-build-pipeline.md).

# Integrating with your container build pipeline

The previous chapter covered the `Dockerfile` side: where `seal fix --os` goes inside an image build and what credentials it needs. This chapter covers the surrounding CI pipeline: how to invoke that build from your CI system, how to pass `SEAL_TOKEN` from the CI's secret store, and how to sequence the sealing step against the rest of your image-delivery workflow.

The sealing step itself is the same regardless of which CI system drives the build. What differs is how each system exposes secrets and how each system invokes `docker build`.

## The general shape

A container build pipeline that runs Seal OS has the same three phases as any other container delivery pipeline:

1. **Build**: `docker build` against a `Dockerfile` that runs `seal fix --os` somewhere in its final stage. `SEAL_TOKEN` and `SEAL_PROJECT` reach the build through `--build-arg` (or BuildKit build secrets).
2. **Test**: optional but recommended. Run the resulting image against your test suite to confirm the sealed packages do not regress runtime behavior.
3. **Push**: tag and push the image to your registry.

The only Seal-specific concern is phase 1. The test and push phases are unchanged.

## Per-CI patterns

The pattern is identical to the patterns documented for application sealing in [Seal Apps via the Seal CLI in CI/CD](/setup-apps-os/cli-in-cicd.md). The only differences are the `Dockerfile` (uses `seal fix --os` instead of, or in addition to, `seal fix`) and the credentials path (always through `--build-arg` or build secrets, since the seal step runs inside `docker build`).

### The `Dockerfile` the CI invokes

Each CI snippet below runs `docker build` against a `Dockerfile` in the repo root. The relevant lines for the seal step are the package install followed by the seal call:

```dockerfile
FROM alpine:3.20

# 1. Install the OS packages your application needs (and any upgrades).
RUN apk add --no-cache curl ca-certificates openssl

# 2. Pull in the Seal CLI.
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal

# 3. Run seal fix --os after the install step, so the full installed set is sealed.
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli

# 4. The rest of the build proceeds against the now-sealed OS layer.
COPY . /app
```

The full `Dockerfile` reference is in [Sealing OS packages in a Dockerfile](/setup-apps-os/seal-os-cicd/dockerfile.md). The CI snippets below show how to invoke `docker build` against this shape.

{% tabs %}
{% tab title="GitHub Actions" %}

```yaml
name: build-and-push
on:
  push:
    branches: [main]

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

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to the registry
        uses: docker/login-action@v3
        with:
          registry: ${{ vars.REGISTRY }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ vars.REGISTRY }}/${{ vars.IMAGE }}:${{ github.sha }}
          build-args: |
            SEAL_PROJECT=my-project-id
          secrets: |
            SEAL_TOKEN=${{ secrets.SEAL_TOKEN }}
```

The token rides through as a BuildKit secret. The `Dockerfile` reads it with `RUN --mount=type=secret,id=SEAL_TOKEN ...`. The `--build-arg` form is also valid; see the [Dockerfile patterns](/setup-apps-os/seal-os-cicd/dockerfile.md) for the `ARG SEAL_TOKEN` shape.
{% endtab %}

{% tab title="GitLab CI" %}
`SEAL_TOKEN` and `REGISTRY_*` are configured as [GitLab CI/CD variables](https://docs.gitlab.com/ee/ci/variables/) (masked and protected). The runner exports them as environment variables; the script references them through the regular shell interpolation.

```yaml
build:
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
    # SEAL_TOKEN injected from the project's masked CI/CD variables.
    # REGISTRY_USERNAME / REGISTRY_PASSWORD likewise.
  before_script:
    - docker login -u "$REGISTRY_USERNAME" -p "$REGISTRY_PASSWORD" "$REGISTRY"
  script:
    - >
      docker build
      --build-arg SEAL_TOKEN="$SEAL_TOKEN"
      --build-arg SEAL_PROJECT=my-project-id
      -t "$REGISTRY/$IMAGE:$CI_COMMIT_SHA"
      .
    - docker push "$REGISTRY/$IMAGE:$CI_COMMIT_SHA"
```

{% endtab %}

{% tab title="Azure DevOps Pipelines" %}
`SEAL_TOKEN` is configured as a [secret pipeline variable](https://learn.microsoft.com/azure/devops/pipelines/process/variables#secret-variables). The `$(SEAL_TOKEN)` syntax expands the value at task-run time; the env mapping makes that explicit.

```yaml
trigger:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

steps:
- task: Docker@2
  displayName: Build and push
  env:
    # Map the secret pipeline variable into the task environment.
    SEAL_TOKEN: $(SEAL_TOKEN)
  inputs:
    command: buildAndPush
    repository: $(IMAGE)
    Dockerfile: '**/Dockerfile'
    tags: $(Build.SourceVersion)
    arguments: --build-arg SEAL_TOKEN=$(SEAL_TOKEN) --build-arg SEAL_PROJECT=my-project-id
```

{% endtab %}

{% tab title="Jenkins" %}

```groovy
pipeline {
  agent any
  environment {
    SEAL_TOKEN = credentials('seal-token')
    SEAL_PROJECT = 'my-project-id'
  }
  stages {
    stage('Build and push') {
      steps {
        sh '''
          docker build \
            --build-arg SEAL_TOKEN=$SEAL_TOKEN \
            --build-arg SEAL_PROJECT=$SEAL_PROJECT \
            -t $REGISTRY/$IMAGE:$BUILD_NUMBER .
          docker push $REGISTRY/$IMAGE:$BUILD_NUMBER
        '''
      }
    }
  }
}
```

Store the token in Jenkins as a [credential](https://www.jenkins.io/doc/book/using/using-credentials/) and reference it with `credentials(...)`.
{% endtab %}

{% tab title="CircleCI" %}
`SEAL_TOKEN` lives in a CircleCI [context](https://circleci.com/docs/contexts/) named `seal-secrets`. The workflow attaches the context to the job, which makes `SEAL_TOKEN` available as an environment variable to every step in the job.

```yaml
version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Build and push
          # SEAL_TOKEN and REGISTRY_* come from the seal-secrets context
          # attached to this job in the workflow below.
          command: |
            echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USERNAME" --password-stdin "$REGISTRY"
            docker build \
              --build-arg SEAL_TOKEN="$SEAL_TOKEN" \
              --build-arg SEAL_PROJECT=my-project-id \
              -t "$REGISTRY/$IMAGE:$CIRCLE_SHA1" .
            docker push "$REGISTRY/$IMAGE:$CIRCLE_SHA1"

workflows:
  build:
    jobs:
      - build:
          context: seal-secrets   # injects SEAL_TOKEN and REGISTRY_* env vars
```

{% endtab %}
{% endtabs %}

## Cache and tag strategy

`seal fix --os` resolves against whatever Seal has published at build time. Treat the image you push as a snapshot of the sealed-package set at that moment, and tag accordingly. Two patterns work:

* **Immutable tags per commit** (the pattern shown above): tag with the commit SHA. The image is reproducible from the same source plus the same `seal fix --os` run timestamp; if a downstream consumer needs to reproduce it later they may not get the same sealed iterations.
* **Floating tag plus periodic rebuild**: tag with `latest` (or `main`) and run the pipeline on a schedule (daily, weekly) regardless of whether the source has changed. Each scheduled build picks up any newer sealed iterations Seal has published since the last build. This is the closest equivalent to `--mode all` with rolling safest tracking.

For regulated environments that need bit-for-bit reproducibility of the image, use `--mode local` or `--mode remote` with pinned Sealing Rules so the sealed iterations the build resolves against are explicit in your source.

## Testing the sealed image

The same image you ship is the one your tests run against. A short verification step in the same pipeline gives you a chance to catch the rare case where a sealed iteration shifts behavior:

```yaml
- name: Smoke test the built image
  run: |
    docker run --rm $REGISTRY/$IMAGE:$TAG /usr/local/bin/server --version
    docker run --rm $REGISTRY/$IMAGE:$TAG /healthcheck.sh
```

For deeper coverage, mount your existing integration tests against the freshly built image rather than against the upstream base image.

## When `seal fix` and `seal fix --os` both run

A pipeline that produces a container image with both application dependencies (npm, Maven, pip, Go, and so on) and OS packages typically runs both `seal fix` (for application dependencies) and `seal fix --os` (for OS packages) inside the same `Dockerfile`.

**Use a different `SEAL_PROJECT` for each.** Each CLI scan tells the Seal Platform which packages it discovered; the next scan against the same project marks every previously-reported package the current scan did not see as stale, so it falls off the Protection page. Because `seal fix` and `seal fix --os` discover disjoint sets (application packages vs. OS packages), sharing a `SEAL_PROJECT` between them makes the second invocation silently bury the first invocation's discovery. The on-disk fixes still happen, but Seal's view of the image's package coverage is reduced to whichever scan ran last.

The recommended layout creates one project per scan, both under the same tenant. For example:

```dockerfile
# 1. Install OS packages
RUN apk add --no-cache curl ca-certificates openssl

# 2. Install application dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# 3. Drop in the CLI
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN

# 4. Seal the application dependencies against the app project
ARG SEAL_PROJECT_APP=my-image-app
RUN SEAL_PROJECT=$SEAL_PROJECT_APP seal fix --mode all

# 5. Seal the OS packages against the OS project
ARG SEAL_PROJECT_OS=my-image-os
RUN SEAL_PROJECT=$SEAL_PROJECT_OS seal fix --os --mode all --remove-cli

# 6. Continue the build
COPY . .
RUN npm run build
```

The two seal steps are independent and can run in either order. Putting `seal fix --os` last (and using `--remove-cli` on it) keeps the CLI in the image for the application-dependency seal step and removes it afterward in one clean motion.

Pass both `SEAL_PROJECT_APP` and `SEAL_PROJECT_OS` as `--build-arg` values from your CI, the same way `SEAL_TOKEN` is passed. The token is shared across both projects; only the project IDs differ.

## Related

* [Sealing OS packages in a Dockerfile](/setup-apps-os/seal-os-cicd/dockerfile.md): the `Dockerfile`-level pattern that this chapter wraps in CI.
* [`seal fix --os` reference](/setup-apps-os/seal-os-cicd/seal-fix-os-reference.md): the flag list.
* [Seal Apps via the Seal CLI in CI/CD](/setup-apps-os/cli-in-cicd.md): the parallel chapter for application sealing, with the same per-CI patterns expanded in more detail.
