SLE BCI Documentation
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

How to build a distroless image using SLE BCI

What is a distroless image?

Distroless images are stripped down container images, where the underlying Linux distribution is reduced to the bare minimum. A distroless image normally contains only certificates and specific core libraries, and it does not include a shell or utilities like cat or ls.

The advantages of distroless images include smaller size and potentially fewer vulnerabilities.

The major disadvantage is the difficulty of debugging a containerized application, as the container image does not provide any debugging tools and may even lack tools to read log files. Debugging applications usually requires attaching sidecar containers or interacting with the container via the /proc/ filesystem. For this reason, we recommend to use SLE BCI Base, SLE BCI Micro or SLE BCI BusyBox as the deployment image, because they make debugging easier, and they come with a valid rpm database.

A distroless image, or any image without the rpm database, makes it harder for container security scanners to find known vulnerabilities in the container image. Keep in mind that if a scanner relies only on the rpm database, it cannot detect a vulnerable shared library.

How to build a distroless images from SLE BCI

The approach described in this section is unsupported and can lead to broken image as only shared libraries will be copied and no other required files are copied.

For a safer method, refer to Deploy an Application using zypper.

Although SUSE does not offer a distroless SLE BCI, you can build one by creating a multi-stage build and creating a final image based on SCRATCH. The following tutorial uses an existing application to demonstrate how to identify its dependent libraries and copy them into the final image.

The first step is to identify all the components required by the application. This can include configuration files, external binaries, and shared libraries. It is your task to determine which configuration files and binaries are required for the program to function correctly. For example, a Python application will require that the Python interpreter is present in the final image.

As minimum, compiled applications are usually dynamically linked to libc. It is possible to statically link against most libraries, but not against glibc (which is the libc implementation used by SLE). Therefore the compiled application requires at least the shared libc library. The required libraries can be identified using ldd. The following example deploys the Rust package manager cargo in an empty image.

Run the ldd /usr/bin/cargo command to obtain all shared libraries against which cargo is linked:

# ldd /usr/bin/cargo
        linux-vdso.so.1 (0x00007ffda3f42000)
        libz.so.1 => /lib64/libz.so.1 (0x00007f3766c14000)
        libcurl.so.4 => /usr/lib64/libcurl.so.4 (0x00007f3767c6a000)
        libssl.so.1.1 => /usr/lib64/libssl.so.1.1 (0x00007f3767bcb000)
        libcrypto.so.1.1 => /usr/lib64/libcrypto.so.1.1 (0x00007f37668d5000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f37666b6000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f3766493000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f3766148000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f3765f44000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f3765b4f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3767ae5000)
        libnghttp2.so.14 => /usr/lib64/libnghttp2.so.14 (0x00007f3765927000)
        libidn2.so.0 => /usr/lib64/libidn2.so.0 (0x00007f376570a000)
        libssh.so.4 => /usr/lib64/libssh.so.4 (0x00007f376549c000)
        libpsl.so.5 => /usr/lib64/libpsl.so.5 (0x00007f376528a000)
        libgssapi_krb5.so.2 => /usr/lib64/libgssapi_krb5.so.2 (0x00007f3765038000)
        libldap_r-2.4.so.2 => /usr/lib64/libldap_r-2.4.so.2 (0x00007f3764de4000)
        liblber-2.4.so.2 => /usr/lib64/liblber-2.4.so.2 (0x00007f3764bd5000)
        libzstd.so.1 => /usr/lib64/libzstd.so.1 (0x00007f37648a5000)
        libbrotlidec.so.1 => /usr/lib64/libbrotlidec.so.1 (0x00007f3764699000)
        libjitterentropy.so.3 => /usr/lib64/libjitterentropy.so.3 (0x00007f3764492000)
        libunistring.so.2 => /usr/lib64/libunistring.so.2 (0x00007f376410f000)
        libkrb5.so.3 => /usr/lib64/libkrb5.so.3 (0x00007f3763e36000)
        libk5crypto.so.3 => /usr/lib64/libk5crypto.so.3 (0x00007f3763c1e000)
        libcom_err.so.2 => /lib64/libcom_err.so.2 (0x00007f3763a1a000)
        libkrb5support.so.0 => /usr/lib64/libkrb5support.so.0 (0x00007f376380b000)
        libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f37635f3000)
        libsasl2.so.3 => /usr/lib64/libsasl2.so.3 (0x00007f37633d6000)
        libbrotlicommon.so.1 => /usr/lib64/libbrotlicommon.so.1 (0x00007f37631b5000)
        libkeyutils.so.1 => /usr/lib64/libkeyutils.so.1 (0x00007f3762fb0000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f3762d87000)
        libpcre.so.1 => /usr/lib64/libpcre.so.1 (0x00007f3762afe000)

The output lists all shared libraries that must be copied into the final image, with one exception: linux-vdso.so.1. This shared library is not present on the file system, it is a virtual shared library exported by the kernel for improved performance. Consult the manpage of vdso via man vdso for more information about linux-vdso.so.1.

The remaining shared libraries are required. We will parse the output of ldd and copy them into a directory while maintaining their directory structure into the final distroless image.

Start with copying the shared libraries that are linked by their name and not their full path as follows:

for lib in $(ldd /usr/bin/cargo | cut -d" " -f 3); do
    install -Dp $lib /l$lib
done

The code above copies all libraries, including their directory structure, into the directory /l/:

# tree /l
/l
|-- lib64
|   |-- libc.so.6
|   |-- libcom_err.so.2
|   |-- libdl.so.2
|   |-- libgcc_s.so.1
|   |-- libm.so.6
|   |-- libpthread.so.0
|   |-- libresolv.so.2
|   |-- libselinux.so.1
|   `-- libz.so.1
`-- usr
    `-- lib64
        |-- libbrotlicommon.so.1
        |-- libbrotlidec.so.1
        |-- libcrypto.so.1.1
        |-- libcurl.so.4
        |-- libgssapi_krb5.so.2
        |-- libidn2.so.0
        |-- libjitterentropy.so.3
        |-- libk5crypto.so.3
        |-- libkeyutils.so.1
        |-- libkrb5.so.3
        |-- libkrb5support.so.0
        |-- liblber-2.4.so.2
        |-- libldap_r-2.4.so.2
        |-- libnghttp2.so.14
        |-- libpcre.so.1
        |-- libpsl.so.5
        |-- libsasl2.so.3
        |-- libssh.so.4
        |-- libssl.so.1.1
        |-- libunistring.so.2
        `-- libzstd.so.1

3 directories, 30 files

We are still missing the shared libraries linked by their full path, in this case that is only /lib64/ld-linux-x86-64.so.2. We can copy them using the following snippet:

for lib in $(ldd /usr/bin/cargo | grep "^[[:space:]]*/" | cut -d" " -f 1); do
    install -Dp $lib /l$lib
done

This makes all necessary libraries available under /l/:

# tree /l
/l
|-- lib64
|   |-- ld-linux-x86-64.so.2
|   |-- libc.so.6
|   |-- libcom_err.so.2
|   |-- libdl.so.2
|   |-- libgcc_s.so.1
|   |-- libm.so.6
|   |-- libpthread.so.0
|   |-- libresolv.so.2
|   |-- libselinux.so.1
|   `-- libz.so.1
`-- usr
    `-- lib64
        |-- libbrotlicommon.so.1
        |-- libbrotlidec.so.1
        |-- libcrypto.so.1.1
        |-- libcurl.so.4
        |-- libgssapi_krb5.so.2
        |-- libidn2.so.0
        |-- libjitterentropy.so.3
        |-- libk5crypto.so.3
        |-- libkeyutils.so.1
        |-- libkrb5.so.3
        |-- libkrb5support.so.0
        |-- liblber-2.4.so.2
        |-- libldap_r-2.4.so.2
        |-- libnghttp2.so.14
        |-- libpcre.so.1
        |-- libpsl.so.5
        |-- libsasl2.so.3
        |-- libssh.so.4
        |-- libssl.so.1.1
        |-- libunistring.so.2
        `-- libzstd.so.1

3 directories, 31 files

With the required information in place, you can create a Dockerfile. The following example uses the SLE BCI Base image as the base image, installs cargo and creates the directory tree shown above:

FROM registry.suse.com/bci/bci-base:15.4 as builder

RUN zypper -n in cargo

RUN for lib in $(ldd /usr/bin/cargo | cut -d" " -f 3); do \
        install -Dp $lib /l$lib; \
     done
RUN for lib in $(ldd /usr/bin/cargo | grep "^[[:space:]]*/" | cut -d" " -f 1); do \
        install -Dp $lib /l$lib; \
     done

Next, copy cargo itself and the libraries under /l/ into an empty image based on SCRATCH. As this image only contains cargo, set both CMD and ENTRYPOINT to cargo, to prevent the container behaving unexpectedly when it is launched without parameters. The complete Dockerfile looks as follows:

FROM registry.suse.com/bci/bci-base:15.4 as builder

RUN zypper -n in cargo

RUN for lib in $(ldd /usr/bin/cargo | cut -d" " -f 3); do \
        install -Dp $lib /l$lib; \
     done
RUN for lib in $(ldd /usr/bin/cargo | grep "^[[:space:]]*/" | cut -d" " -f 1); do \
        install -Dp $lib /l$lib; \
     done

FROM scratch

COPY --from=builder /l/ /
COPY --from=builder /usr/bin/cargo /usr/bin/cargo
ENTRYPOINT ["/usr/bin/cargo"]

Build the image with the preferred container runtime:

docker build -t cargo .
buildah bud --layers -t cargo .
nerdctl build -t cargo .

This creates a fully containerized ready-to-use cargo container:

❯ docker run --rm -it cargo help
Rust's package manager

Usage: cargo [OPTIONS] [COMMAND]

Options:
  -V, --version             Print version info and exit
      --list                List installed commands
      --explain <CODE>      Run `rustc --explain CODE`
  -v, --verbose...          Use verbose output (-vv very verbose/build.rs output)
  -q, --quiet               Do not print cargo log messages
      --color <WHEN>        Coloring: auto, always, never
      --frozen              Require Cargo.lock and cache are up to date
      --locked              Require Cargo.lock is up to date
      --offline             Run without accessing the network
      --config <KEY=VALUE>  Override a configuration value
  -Z <FLAG>                 Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
  -h, --help                Print help information

Some common cargo commands are (see all commands with --list):
    build, b    Compile the current package
    check, c    Analyze the current package and report errors, but don't build object files
    clean       Remove the target directory
    doc, d      Build this package's and its dependencies' documentation
    new         Create a new cargo package
    init        Create a new cargo package in an existing directory
    add         Add dependencies to a manifest file
    remove      Remove dependencies from a manifest file
    run, r      Run a binary or example of the local package
    test, t     Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this package to the registry
    install     Install a Rust binary. Default location is $HOME/.cargo/bin
    uninstall   Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.
❯ podman run --rm -it localhost/cargo help
Rust's package manager

Usage: cargo [OPTIONS] [COMMAND]

Options:
  -V, --version             Print version info and exit
      --list                List installed commands
      --explain <CODE>      Run `rustc --explain CODE`
  -v, --verbose...          Use verbose output (-vv very verbose/build.rs output)
  -q, --quiet               Do not print cargo log messages
      --color <WHEN>        Coloring: auto, always, never
      --frozen              Require Cargo.lock and cache are up to date
      --locked              Require Cargo.lock is up to date
      --offline             Run without accessing the network
      --config <KEY=VALUE>  Override a configuration value
  -Z <FLAG>                 Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
  -h, --help                Print help information

Some common cargo commands are (see all commands with --list):
    build, b    Compile the current package
    check, c    Analyze the current package and report errors, but don't build object files
    clean       Remove the target directory
    doc, d      Build this package's and its dependencies' documentation
    new         Create a new cargo package
    init        Create a new cargo package in an existing directory
    add         Add dependencies to a manifest file
    remove      Remove dependencies from a manifest file
    run, r      Run a binary or example of the local package
    test, t     Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this package to the registry
    install     Install a Rust binary. Default location is $HOME/.cargo/bin
    uninstall   Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.
❯ nerdctl run --rm -it cargo help
Rust's package manager

Usage: cargo [OPTIONS] [COMMAND]

Options:
  -V, --version             Print version info and exit
      --list                List installed commands
      --explain <CODE>      Run `rustc --explain CODE`
  -v, --verbose...          Use verbose output (-vv very verbose/build.rs output)
  -q, --quiet               Do not print cargo log messages
      --color <WHEN>        Coloring: auto, always, never
      --frozen              Require Cargo.lock and cache are up to date
      --locked              Require Cargo.lock is up to date
      --offline             Run without accessing the network
      --config <KEY=VALUE>  Override a configuration value
  -Z <FLAG>                 Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
  -h, --help                Print help information

Some common cargo commands are (see all commands with --list):
    build, b    Compile the current package
    check, c    Analyze the current package and report errors, but don't build object files
    clean       Remove the target directory
    doc, d      Build this package's and its dependencies' documentation
    new         Create a new cargo package
    init        Create a new cargo package in an existing directory
    add         Add dependencies to a manifest file
    remove      Remove dependencies from a manifest file
    run, r      Run a binary or example of the local package
    test, t     Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this package to the registry
    install     Install a Rust binary. Default location is $HOME/.cargo/bin
    uninstall   Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.