> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Container images

> Package binaries into OCI container images with oci_image, and push them to a registry with oci_push — no Docker CLI required.

rbs builds OCI-compliant container images **without Docker**. The `oci_image` rule
packages a built binary (plus any extra files) into a Docker-compatible tar you can
`docker load` or `podman load`, and `oci_push` uploads it to a registry. Building
stays pure — the push is a deliberate side effect that only happens at `rbs run`.

The rules live in one module:

```python theme={null}
load("@rbs//oci/rules.rbs", "oci_image", "oci_tarball", "oci_push")
```

## A service image, end to end

The typical shape: cross-compile the binary for the cluster's platform, package it
on a minimal base, and declare the push target next to it.

```python theme={null}
load("@rbs//go/rules.rbs", "go_binary")
load("@rbs//oci/rules.rbs", "oci_image", "oci_push")

# Linux build for the container image (the cluster is linux/arm64 here)
go_binary(
    name = "server_linux",
    srcs = glob(["cmd/**/*.go", "internal/**/*.go"]) + ["go.mod", "go.sum"],
    pkg = "./cmd/server",
    goos = "linux",
    goarch = "arm64",
)

oci_image(
    name = "image",
    binary = ":server_linux",
    base = "scratch",
    entrypoint = ["/app/server_linux/runfiles/_main/server_linux.bin"],
    env = {"PORT": ":8090"},
    ports = ["8090"],
    labels = {"org.opencontainers.image.title": "my-server"},
)

oci_push(
    name = "push",
    image = ":image",
    repository = "registry.example.com/myorg/my-server",
    tag = "latest",
)
```

```bash theme={null}
rbs build //services/myservice:image     # build the image tar
rbs run //services/myservice:push        # build (if needed) and push it
```

The built tar lands in your workspace at
`.rbs/bin/<platform>/<package>/<name>/<name>.tar` and loads straight into a local
daemon:

```bash theme={null}
docker load < .rbs/bin/darwin-arm64/services/myservice/image/image.tar
```

## oci\_image

Creates an OCI image from a binary target and optional extra files. The packaged
binary's **entire output directory** (launcher, `runfiles/`, toolchain links) is
copied into the image at `/app/<binary>/`, with build caches excluded, so the
binary runs the same way in the container as it does under `rbs run`.

| Attribute    | Type        | Default       | Required | Meaning                                                                                                                                                                                       |
| ------------ | ----------- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`       | string      | —             | yes      | Target name. Also names the output tar and the default tag.                                                                                                                                   |
| `binary`     | label       | —             | no       | Binary target to package (e.g. `:server_linux`). Built before the image.                                                                                                                      |
| `base`       | string      | `"scratch"`   | no       | Base image: `"scratch"` (empty, no OS), or any registry reference — `ubuntu:22.04`, `alpine/git:latest`, `ghcr.io/owner/repo:tag`. Base layers are stacked beneath yours.                     |
| `entrypoint` | string list | launcher      | no       | Container entrypoint. Defaults to the packaged binary's launcher script at `/app/<binary>/<binary>`.                                                                                          |
| `cmd`        | string list | `[]`          | no       | Default command arguments.                                                                                                                                                                    |
| `env`        | string dict | `{}`          | no       | Environment variables baked into the image config.                                                                                                                                            |
| `workdir`    | string      | `"/app"`      | no       | Working directory. Defaults to `/app/<binary>` when a binary is packaged.                                                                                                                     |
| `user`       | string      | —             | no       | User the container runs as.                                                                                                                                                                   |
| `labels`     | string dict | `{}`          | no       | OCI image labels.                                                                                                                                                                             |
| `files`      | label list  | `[]`          | no       | Additional files, added as their own layer. Plain paths (resolved from the workspace root, not the package) land at `/app/<filename>`; `{"src": ..., "dst": ...}` dicts pick the destination. |
| `ports`      | string list | `[]`          | no       | Ports to expose.                                                                                                                                                                              |
| `volumes`    | string list | `[]`          | no       | Volume mount points.                                                                                                                                                                          |
| `tag`        | string      | `name:latest` | no       | Tag stamped into the tar — what `docker load` reports.                                                                                                                                        |
| `os`         | string      | `"linux"`     | no       | Image OS stamp. Validated against the packaged binary's header when inspectable — a mismatch fails the build.                                                                                 |
| `arch`       | string      | derived       | no       | Image architecture stamp (`"amd64"`, `"arm64"`). Empty derives it from the packaged binary, falling back to the host architecture.                                                            |

### The image platform is checked against your binary

`oci_image` reads the packaged binary's own executable header to decide (and
verify) the image's `os`/`architecture` stamp. Packaging a macOS binary into an
image stamped `linux` is a **build error**, not a surprise at pod-scheduling time
— cross-compile the binary for the image platform instead (for Go, that is
`go_binary`'s `goos`/`goarch` attributes).

### Working with `scratch` and other minimal bases

Two things bite everyone once, so they are worth knowing up front:

* **`scratch` has no shell.** The default entrypoint is the binary's launcher
  *script*, which needs one. On `scratch`, point `entrypoint` at the real
  executable inside the packaged directory: `/app/<binary>/runfiles/_main/<binary>.bin`.
* **Your image config replaces the base image's config wholesale** — the base
  contributes layers only. If the base relied on its config (a `PATH`, an
  entrypoint, default env), re-declare what you need in `env`/`entrypoint`.
  A base like `alpine/git` won't find `git` at runtime unless you set `PATH`
  yourself.

`scratch` also carries no CA certificates: a service that makes outbound TLS
connections needs a base that ships them (or a mounted certificate bundle).

### Base images are cached user-globally

Base images referenced by `base` are pulled from the registry once and cached in
the user-global cache (`~/.cache/rbs/oci-images`, or under `$RBS_CACHE_DIR` when
set) — shared by every workspace on the machine. Layer content is
checksum-verified both when fetched and when loaded from cache, and the cache is
swept by the same age bounds as everything else under
[`rbs cache gc`](/build/caching).

<Note>
  Referencing a locally built tar as a base (`base = "@some_target"`) is not
  supported yet — `base` must be `"scratch"` or a registry reference.
</Note>

## oci\_tarball

Packages a plain directory (rather than a binary target) into a Docker-compatible
image tar. Useful for static assets or prepared filesystem trees.

```python theme={null}
oci_tarball(
    name = "site",
    directory = ":dist",
    layer_dst = "/usr/share/nginx/html",
    tag = "site:latest",
)
```

| Attribute    | Type        | Default       | Required | Meaning                                             |
| ------------ | ----------- | ------------- | -------- | --------------------------------------------------- |
| `directory`  | label       | —             | no       | Directory to package.                               |
| `layer_dst`  | string      | `"/"`         | no       | Destination path of the directory inside the image. |
| `entrypoint` | string list | `[]`          | no       | Container entrypoint.                               |
| `cmd`        | string list | `[]`          | no       | Default command arguments.                          |
| `env`        | string dict | `{}`          | no       | Environment variables.                              |
| `workdir`    | string      | `"/"`         | no       | Working directory.                                  |
| `labels`     | string dict | `{}`          | no       | OCI image labels.                                   |
| `tag`        | string      | `name:latest` | no       | Image tag.                                          |

## Pushing to a registry with oci\_push

`oci_push` declares *where* an image goes; the push itself happens only when you
`rbs run` the target. There is no `rbs push` subcommand — pushing is a run-time
side effect, keeping `rbs build` pure and repeatable.

```bash theme={null}
rbs run //services/myservice:push
```

| Attribute    | Type        | Default     | Required | Meaning                                                                                 |
| ------------ | ----------- | ----------- | -------- | --------------------------------------------------------------------------------------- |
| `image`      | label       | —           | yes      | The `oci_image` target to push.                                                         |
| `repository` | string      | —           | no       | Explicit repository (`host/name`). Omit when resolving through infra.                   |
| `uses`       | string list | `[]`        | no       | An infra `container_registry` resource whose applied state supplies the repository URL. |
| `tag`        | string      | `"latest"`  | no       | Tag to push.                                                                            |
| `workspace`  | string      | `"default"` | no       | Infra workspace to resolve `uses` against.                                              |

Either `repository` or `uses` must resolve to a repository — a push target with
neither fails with a clear error.

### Credentials

The push authenticates from environment variables in the pushing shell:

```bash theme={null}
export RBS_OCI_USERNAME=me RBS_OCI_PASSWORD=...   # username/password
# or
export RBS_OCI_TOKEN=...                          # pre-issued bearer token (wins if set)
```

For ECR, use `RBS_OCI_USERNAME=AWS` with a password from
`aws ecr get-login-password`. Local registries on `localhost` need no
credentials.

### Resolving the repository from infra

When your registry is provisioned through
[rbs infrastructure](/infra/overview), skip the hardcoded `repository` and let
the push target read it from applied state:

```python theme={null}
oci_push(
    name = "push",
    image = ":image",
    uses = ["registry"],   # a container_registry resource declared in this workspace
)
```

After `rbs infra apply` creates (or confirms) the registry, the push target
resolves its repository URL from the resource's exported `REGISTRY_URL` — a new
service's push target needs no editing when registries move. Kubernetes deploy
targets resolve image references through the same path, so what you push and
what you deploy can never disagree — see
[Deploying to Kubernetes](/deploy/kubernetes).
