Swift app on distroless images

Hey all,

I wanted to share some experience with experimenting running a server side swift app on distroless images. This produces relatively small container images ideal for deploying to k8s clusters.

The app is a simple Swift executable that uses some NIO to host a webserver and receives web hook events from GitHub, it then communicates with the k8s API (thanks to swiftkube/client) and eventually calls out to the system installed helm binary with Foundation.Process (the latter is a pain to get working reliably on linux ...)

We use FROM swift:5.7.1-focal as builder to build the Swift executable with -Xswiftc -static-stdlib flag.

With this the app runs reliably when deployed to the gcr.io/distroless/cc-debian11 base image.

We also tried compiling with -static-executable and deploying to gcr.io/distroless/base, but expectedly this fails at runtime with segmentation faults, most likely because the glibc used for building is not exactly the same as bundled with the base image.

So, long story short, building -static-stdlib on ubuntu:focal and deploying to distroless/cc gets you everything you need to run a simple Swift app in a small image. For us, going distroless saved around 25MB image size compared to a ubuntu:focal runtime image.

If you have additional runtime dependencies, for example, on libz, you need to copy the dynamic library to the destination image together with your binary, eg:

RUN ldd my-binary | grep libz | awk '{ print $3 }' | xargs -I{} cp {} /app

# ...

FROM gcr.io/distroless/cc-debian11

COPY --from=builder /app/*  /app/
WORKDIR /app
ENTRYPOINT [ "/app/my-binary" ]

Is there a way to explicitly statically link some specific library into the executable, while keeping the other dependencies dynamic?

4 Likes

How exactly is the library provided to your executable? Is it specified as a dependency in your Package.swift, and how if so?

Hey Max,

the dependency is declared as a "system library" target.

.systemLibrary(name: "CZlib"),

with the usual module.modulemap in Sources/CZlib:

 module CZlib {
    header "shim.h"
    link "z"
    export *
 }

Have you tried specifying an absolute path to your static library? For example, this should work for swift:5.7-jammy container on aarch64 with zlib1g-dev package installed:

 module CZlib {
    header "shim.h"
    link "/usr/lib/aarch64-linux-gnu/libz.a"
    export *
 }

Interesting, I will give it a try. thank you.

Hey @t089, Process sure is a PITA. Though the helpers that we made in Vapor/Console sure make is a little more bearable. If these kind of helpers were public in Console-Kit, I'm sure that'd be a great start. But for now it serves as a good example.

Hey @Joannis_Orlandos thanks for pointing. These helpers definitely look useful, but I think they might be a bit too simple.

Eg.

    static func run(_ program: String, _ arguments: [String]) throws -> String {
        let task = try Self.new(program, arguments)
        try task.runUntilExit()
        return task.stdout.read()
    }

This can block and hang indefinitely if program produces more output that fits in the system i/o buffers: if program writes to standard output, but nobody reads from standard output, at some point the write calls will just block and program will never exit. But the Process instance waits for program to exit before it starts reading -> deadlock. I guess for "small, script like" programs it would be mostly fine, but it can fail miserably in some other cases.

I totally understand and agree, but it’s a quick way to get started and okay for most use cases. I just wanted to give you a heads up that these helpers exist, and can help point you in the right direction.

Yeah I get it, I just suffered through this myself, starting with something similar to these helpers but then seeing my app hang seemingly randomly on some events. That's why I feel they are very misleading, but I also acknowledge that in many of Apple's example code the same pattern is used, eg in build tool plugins...

I think it is telling that SPM itself actually avoids using Foundation.Process but builds an abstraction on top of the POSIX functions directly (at least for Linux and macOS, Windows uses Foundation :man_shrugging:).