Swift Vapor reducing Docker image size

Good Day everyone,

Im currently working on a server side project developed using "Vapor". Currently we are able to reduce the image size to 183 mb(which is think is pretty darn good) using swift:5.5-focal and running in ubuntu:focal. But as per specifications from the project architects we have to try and reduce the size even more.

I have tried using the "slim" versions but I there seems to be some problems resolving some package dependencies from Vapor.

My question is, is there any set of tools that I can look into to strip even more the image?

3 Likes

What does your Dockerfile look like?

You may want to try this: Idea: runtime/slim images - #30 by ratranqu

It’s a bit old now, but should still work.

1 Like

I just played around with this a bit, and have been able to get the Vapor hello word example (created with vapor new hello -n) down from 190MB to 115MB, 105MB being used by the compiled app binary.

I wouldn't really recommend running this in production without further testing, but I changed lines 46-54 in the example Dockerfile to the following:

FROM busybox:glibc

COPY --from=build /usr/sbin/tzconfig /usr/sbin/tzconfig
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=build /lib/x86_64-linux-gnu/libutil.so.1 /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/

RUN adduser -Dh /app vapor

Which is to say, I changed the image to the glibc variant of busybox, copied all libraries the app binary links, plus timezone and certificate data, from the builder image, and changed the user creation so it works with busybox.

1 Like
# ================================
# Build image
# ================================
FROM swift:5.5-focal as build

# Install OS updates and, if needed, sqlite3
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
    && apt-get -q update \
    && apt-get -q dist-upgrade -y \
    && rm -rf /var/lib/apt/lists/*

# Set up a build area
WORKDIR /build

# First just resolve dependencies.
# This creates a cached layer that can be reused
# as long as your Package.swift/Package.resolved
# files do not change.
COPY ./Package.* ./
RUN swift package resolve

# Copy entire repo into container
COPY . .

# Build everything, with optimizations
RUN swift build -c release --static-swift-stdlib

# Switch to the staging area
WORKDIR /staging

# Copy main executable to staging area
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./

# Copy any resources from the public directory and views directory if the directories exist
# Ensure that by default, neither the directory nor any of its contents are writable.
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true

# ================================
# Run image
# ================================
FROM ubuntu:focal

# Make sure all system packages are up to date.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && \
    apt-get -q update && apt-get -q dist-upgrade -y && apt-get -q install -y ca-certificates && \
    rm -r /var/lib/apt/lists/*

# Create a vapor user and group with /app as its home directory
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor

# Switch to the new home directory
WORKDIR /app

# Copy built executable and any staged resources from builder
COPY --from=build --chown=vapor:vapor /staging /app

# Ensure all further commands run as the vapor user
USER vapor:vapor

# Let Docker bind to port 8080
EXPOSE 8080

# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

This actually helped a lot, I need to do more testing but as of now the basic functions of the server are working fine. I still want to try @ratranqu answer and compared sizes.

Seems like the other example is using busybox too.

2 Likes

Switching to swift:5.6-focal might give you a small improvement but you're probably close to as good as you can get for now for 'recommended' containers. Swift needs to link the standard library and anything you use, so that plus the binary size plus Ubuntu is fairly hefty too. Ubuntu is required as opposed to something like Alpine because of Glibc and ICU requirements

I have a running vapor app image that’s 72.4MB. I can get into more details if you need but the general idea is to build the project with static linking of the stdlib and use a Debian slim as the base in which to copy the binary. Alpine instead of Debian does not work because of glibc as expected.

1 Like

I will try this too. Thankfully I'm in the middle of a research spike with this so I can experiment as much as I can.

Would you mind showing me an example of a dockerfile with your method.

Run to this problem:

=> ERROR [stage-1 4/4] RUN tar -xzvf /tmp/swift_libs.tar.gz && rm -rf /tmp/*                                                                                                              0.8s 
------                                                                                                                                                                                          
 > [stage-1 4/4] RUN tar -xzvf /tmp/swift_libs.tar.gz && rm -rf /tmp/*:                                                                                                                         
#20 0.763 usr/lib/swift/linux/libswift_Concurrency.so                                                                                                                                           
#20 0.763 usr/lib/swift/linux/libswiftCore.so                                                                                                                                                   
#20 0.763 usr/lib/swift/linux/libswiftGlibc.so                                                                                                                                                  
#20 0.763 lib/x86_64-linux-gnu/libm.so.6                                                                                                                                                        
#20 0.763 lib/x86_64-linux-gnu/libpthread.so.0
#20 0.763 lib/x86_64-linux-gnu/libutil.so.1
#20 0.763 lib/x86_64-linux-gnu/libdl.so.2
#20 0.763 usr/lib/swift/linux/libswiftDispatch.so
#20 0.763 usr/lib/swift/linux/libdispatch.so
#20 0.763 usr/lib/swift/linux/libBlocksRuntime.so
#20 0.763 usr/lib/swift/linux/libFoundation.so
#20 0.763 usr/lib/x86_64-linux-gnu/libstdc++.so.6
#20 0.763 lib/x86_64-linux-gnu/libz.so.1
#20 0.763 lib/x86_64-linux-gnu/libgcc_s.so.1
#20 0.763 lib/x86_64-linux-gnu/libc.so.6
#20 0.763 lib64/ld-linux-x86-64.so.2
#20 0.763 lib/x86_64-linux-gnu/librt.so.1
#20 0.763 usr/lib/swift/linux/libicuucswift.so.65
#20 0.763 usr/lib/swift/linux/libicui18nswift.so.65
#20 0.763 usr/lib/swift/linux/libicudataswift.so.65
#20 0.764 rm: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by rm)
#20 0.764 rm: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by rm)

The error is triggered by 'rm -rf /tmp/*'. Did you face a similar problem?

Cool!
As a reference here’s my Dockerfile. It’s heavily based on Vapor’s. You can ignore all of the SQLite stuff; the version of SQLite in Debian has a bug and I needed a newer one w/o this particular bug.

# ================================
# Build image
# ================================
FROM swift:5.6 as build

# Install OS updates and, if needed, sqlite3
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
	&& apt-get -q update \
	&& apt-get -q dist-upgrade -y \
	&& apt-get install -y libsqlite3-dev \
	&& rm -rf /var/lib/apt/lists/*

# Set up a build area
WORKDIR /build

# First just resolve dependencies.
# This creates a cached layer that can be reused
# as long as your Package.swift/Package.resolved
# files do not change.
COPY ./Package.* ./
RUN swift package resolve

# Copy entire repo into container
COPY . .

# Build everything, with optimizations
RUN swift build -c release --static-swift-stdlib

# Switch to the staging area
WORKDIR /staging

# Copy main executable to staging area
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./

# Copy any resouces from the public directory and views directory if the directories exist
# Ensure that by default, neither the directory nor any of its contents are writable.
RUN [ -d /build/Public    ] && { mv /build/Public    ./Public    && chmod -R a-w ./Public;    } || true
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true


# ================================
# Build sqlite 3.37.2
# ================================
# We need a newer version of sqlite than provided by Debian…
FROM debian:11-slim as build-sqlite

ARG SQLITE_VERSION=3370200

# Make sure all system packages are up to date and install sqlite3’s dependencies.
# Note that I tried to disable TCL, but it’s still required somehow.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
	&& apt-get -q update \
	&& apt-get -q dist-upgrade -y \
	&& apt-get install -y \
		build-essential \
		ca-certificates \
		clang \
		openssl \
		tcl-dev \
		unzip \
		wget \
	&& rm -rf /var/lib/apt/lists/*

RUN wget "https://sqlite.org/2022/sqlite-src-$SQLITE_VERSION.zip" \
	&& unzip "sqlite-src-$SQLITE_VERSION.zip"
RUN cd "sqlite-src-$SQLITE_VERSION" \
	&& ./configure --prefix=/sqlite-build --disable-tcl --disable-readline --without-tcl \
	&& make install
RUN tar -C /sqlite-build -cvf /sqlite.tar.bz2 .


# ================================
# Run image
# ================================
FROM debian:11-slim

# Make sure all system packages are up to date.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
	&& apt-get -q update \
	&& apt-get -q dist-upgrade -y \
	&& rm -rf /var/lib/apt/lists/*

# Install sqlite3 from builder-sqlite
COPY --from=build-sqlite /sqlite.tar.bz2 /sqlite.tar.bz2
RUN cd / && tar xvf sqlite.tar.bz2 && rm sqlite.tar.bz2

# Create a vapor user and group with /app as its home directory
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor

# Switch to the new home directory
WORKDIR /app

# Copy built executable and any staged resources from builder
COPY --from=build --chown=vapor:vapor /staging /app

# Ensure all further commands run as the vapor user
USER vapor:vapor

# Let Docker bind to port 8080
EXPOSE 8080

# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
1 Like

Devs, I think that I got to a good point in terms of size. To wrapped this post I will share my findings:

First I will like to start with @ahti 's dockefile. This is the current one that gave me the best results (112.58 MB) :

The second lead was was a link from a post from @ratranqu, I had some challenges making it work so I'm going to post the things needed for this.

In my case this method generates an image with a size of 117MB. Which is still damn good and I'm pretty sure you can find a use for it.

The main idea is to extract the needed dependencies of the swift application and compressed them, then use them in the running environment of our choosing, for my case I picked busybox since it was used by the tutorial and is super lightweight.

This is the shell script that takes the name of the binary and optionally the name of an output file, which uses ldd to determine the shared libraries used by the binary and compresses them into a gzipped tar file:
A Minimal Swift Docker Image
Swift With Docker

BIN="$1"
OUTPUT_TAR="${2:-swift_libs.tar.gz}"
TAR_FLAGS="hczvf"
DEPS=$(ldd $BIN | awk 'BEGIN{ORS=" "}$1\
~/^\//{print $1}$3~/^\//{print $3}'\
| sed 's/,$/\n/')
tar $TAR_FLAGS $OUTPUT_TAR $DEPS

make sure to name it "pkg-swift-deps.sh"

Next is the dockerfile:

FROM swift:5.5-focal

# Install OS updates
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
    && apt-get -q update \
    && apt-get -q dist-upgrade -y \
    && rm -rf /var/lib/apt/lists/*

# Create the app folder and make it the working directory.
ADD . /app
WORKDIR /app

# Resolve swift packages and build the app with static stdlib.
RUN swift package resolve
RUN swift build -c release --static-swift-stdlib

# Copy  the pkg-swift-deps shell script to bin folder
# Use chmod to give permissions as an executable
# Run the shell script. We need to pass the location of the binary of the app, because the script is running for release we have to use ‘.build/release’
COPY pkg-swift-deps.sh /usr/bin/pkg-swift-deps
RUN chmod +x /usr/bin/pkg-swift-deps
RUN pkg-swift-deps .build/release/<NAME_OF_YOUR_APP_EXECUTABLE>

# Use busybox stable glibc. Had to use this stable version due to the ‘rm’ command giving me some issues
FROM busybox:stable-glibc

# Copy the compressed .tar file generated from the script, from=0 means that we are coping this from the first staged (FROM swift:5.5-focal)
# Copy the executable into ‘/usr/bin/‘
COPY --from=0 app/swift_libs.tar.gz /tmp/swift_libs.tar.gz
COPY --from=0 app/.build/release/<NAME_OF_YOUR_APP_EXECUTABLE> /usr/bin/

# Standard user creation for vapor
RUN adduser -Dh /app vapor

# unpack the dependencies from the tar file and remove  the temp folder
RUN tar -xzvf /tmp/swift_libs.tar.gz && \
    rm -rf /tmp/*

# Ensure all further commands run as the vapor user
USER vapor:vapor

# Let Docker bind to port 8080
EXPOSE 8080

# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["<NAME OF YOUR EXECUTABLE>"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

The final lead was the docker file given by @Frizlab (It produced an image of 187 but again is a good example):

2 Likes

Alpine support would help here.

2 Likes