> 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/dockerfile.md).

# Sealing OS packages in a Dockerfile

The canonical place to run `seal fix --os` is inside a `Dockerfile` `RUN` step, after the base image's package manager has installed the OS packages your application needs and before the build does anything that drops privileges or finalizes the image.

The CLI auto-detects the distribution and dispatches to the matching OS-package family. One pattern covers Alpine, Debian / Ubuntu, and the Red Hat family alike; the only thing that changes is which base image you started from.

## Pattern

```dockerfile
# Whatever base image your application uses.
FROM alpine:3.20

# Install the OS packages your application needs, normally.
RUN apk add --no-cache curl ca-certificates openssl

# Drop the Seal CLI into the image and seal the OS packages.
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli

# Continue the build with the sealed OS packages in place.
COPY . /app
WORKDIR /app
CMD ["/app/server"]
```

What the relevant lines do:

1. `ADD --chmod=755 ... /usr/local/bin/seal` downloads the latest CLI binary into the image and marks it executable.
2. `ARG SEAL_TOKEN` and `ARG SEAL_PROJECT` accept the credentials from the `docker build` invocation. The variables are available only during the build; they do not persist into the final image.
3. `RUN seal fix --os --mode all` detects the distribution, identifies which installed OS packages have sealed versions available, and replaces them in place.
4. `--remove-cli` deletes the CLI binary from the image after the run so it does not ship in the final image. Drop the flag if you want to keep the CLI available at runtime (for example, to run scans against the running container).

## Passing credentials from the build

Pass `SEAL_TOKEN` from your CI's secret store as a build argument:

```bash
docker build \
  --build-arg SEAL_TOKEN=$SEAL_TOKEN \
  --build-arg SEAL_PROJECT=my-project-id \
  -t my-image .
```

In CI, the host-side `SEAL_TOKEN` is your CI's secret-store-backed environment variable. `--build-arg` propagates it into the `Dockerfile`'s `RUN seal fix --os` step without writing it to a layer.

For BuildKit-enabled builds, the more secure form uses build secrets instead of `--build-arg`. The build argument form above is the simpler default; the BuildKit form is documented in the CI integration page.

## Per-distribution notes

The CLI handles the distribution-specific package manager work for you. There are still a few base-image conventions worth respecting in the surrounding `Dockerfile`:

{% tabs %}
{% tab title="Alpine (apk)" %}

```dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates openssl
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli
```

* `apk add --no-cache` is the idiomatic install line; the CLI works against the resulting package database.
* Alpine images are small; the CLI's network round-trips to `apk.sealsecurity.io` are the dominant cost of the seal step.
  {% endtab %}

{% tab title="Debian / Ubuntu (apt)" %}

```dockerfile
FROM debian:bookworm-slim
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl ca-certificates openssl && \
    rm -rf /var/lib/apt/lists/*
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli
```

* The CLI updates the apt source list itself when it needs to pull from `deb.sealsecurity.io`. You do not need to add a Seal entry to `/etc/apt/sources.list` by hand.
* Keep the install + clean-up on one layer (`apt-get update && apt-get install && rm -rf /var/lib/apt/lists/*`) the way you normally would; the seal step runs after that layer is committed.
  {% endtab %}

{% tab title="RHEL / CentOS / Amazon Linux (rpm)" %}

```dockerfile
FROM redhat/ubi9:latest
RUN dnf install -y curl ca-certificates openssl && \
    dnf clean all
ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli
```

* Works with `dnf` (modern RHEL family) and `yum` (older RHEL family) alike. The CLI detects which one is present.
* On Amazon Linux 2 use `RUN yum install ...` instead of `dnf`. The CLI behavior is the same.
  {% endtab %}
  {% endtabs %}

## Multi-stage builds

In a multi-stage build, run `seal fix --os` in the **final** stage, the one whose layers ship in the published image. Sealing the OS packages of a builder stage that is later discarded is wasted work and does not protect the runtime image.

```dockerfile
# Builder stage. No need to seal here.
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/server ./cmd/server

# Runtime stage. Seal the OS packages here.
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /out/server /usr/local/bin/server

ADD --chmod=755 https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest /usr/local/bin/seal
ARG SEAL_TOKEN
ARG SEAL_PROJECT=my-project-id
RUN seal fix --os --mode all --remove-cli

CMD ["/usr/local/bin/server"]
```

## Layer ordering and caching

`docker build` caches each `RUN` step's result by hashing the step's inputs. The `seal fix --os` step's output depends on the sealed-package set Seal currently publishes for your distribution, which Docker has no way to know about: from Docker's perspective, the step's inputs (the previous layer, the command string, the `ARG` values) might look identical between two builds even when Seal has published a new sealed iteration.

Two ways to manage that:

* **Pin to specific sealed iterations.** Use `--mode remote` or `--mode local` with explicit Sealing Rules. Bumping a rule changes the rule contents the CLI reads at build time, which propagates into the layer cache invalidation chain.
* **Bust the cache periodically.** Add a no-op `ARG` like `ARG SEAL_REBUILD_TOKEN=1` and pass `--build-arg SEAL_REBUILD_TOKEN=$(date +%Y%m%d)` from CI so every build at least once a day re-runs the seal step against the latest sealed iterations.

The right approach depends on whether your team's mental model for "what's in this image" is pinned (rules) or rolling (latest safest).

## Verify

After the build finishes, run the produced image and ask the OS package manager for the version of any sealed package:

{% tabs %}
{% tab title="Alpine" %}

```bash
docker run --rm my-image apk info -v openssl
```

The reported version differs from the upstream origin version in a way that identifies the sealed iteration. The exact form is Alpine-specific and documented in Seal's naming and versioning conventions.
{% endtab %}

{% tab title="Debian / Ubuntu" %}

```bash
docker run --rm my-image dpkg-query -W openssl
```

The version field carries the sealed-iteration suffix (`+spN`).
{% endtab %}

{% tab title="RHEL family" %}

```bash
docker run --rm my-image rpm -q openssl
```

The package release field carries the sealed-iteration suffix (`+spN`).
{% endtab %}
{% endtabs %}

The exact version-string format per OS-package family is documented in Seal's naming and versioning conventions reference.

## Common pitfalls

* **The CLI runs before all `apk add` / `apt-get install` / `dnf install` steps.** The packages installed after the seal step ship in their upstream vulnerable form. Place `seal fix --os` after every step that installs or upgrades OS packages so the full installed set is sealed.
* **The image drops to a non-root user before the CLI runs.** The seal step needs to write to the package-manager database. Run `seal fix --os` while the build is still `USER root`, then drop privileges with a later `USER` directive.
* **The base image is one Seal does not support.** The CLI exits non-zero with a message naming the detected distribution. The supported distributions are the `apk`, `deb`, and `rpm` families enumerated in the [`seal fix --os` reference](/setup-apps-os/seal-os-cicd/seal-fix-os-reference.md).

## Related

* [`seal fix --os` reference](/setup-apps-os/seal-os-cicd/seal-fix-os-reference.md): the full flag list.
* [Running the CLI inside a Dockerfile](/setup-apps-os/cli-in-cicd/dockerfile.md): the parallel pattern for `seal fix` (application dependencies). Both can run in the same `Dockerfile`.
* [Integrating with your container build pipeline](/setup-apps-os/seal-os-cicd/container-build-pipeline.md): wiring the `docker build` into your CI system.
