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.